Skip to content

Commit 9f53980

Browse files
authored
[URECA-75] Feat: 북마크 토글 API 구현 (#49)
* feat: summary controller에 북마크 토글 API ì¶ì 추가 * feat: summary service에 북마크 목록 추가 * feat: summary mapper에 북마크 상태 및 토글 추가 * fix: 북마크 API 가능하도록 P ì±CORS 허용에ATC추가DDD * feat: 로그인, 로/¸아웃 ì� 레거시 RT 삭제 기능 추가� * feat: path/samSite 입력값 방어 추가
1 parent ba58afb commit 9f53980

8 files changed

Lines changed: 99 additions & 90 deletions

File tree

src/main/java/com/ureca/unity/domain/auth/controller/LogoutController.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,14 @@ public void logout(
3030
HttpServletResponse response
3131
) {
3232
logoutService.logout(refreshToken);
33+
34+
// 1. 현재 표준 쿠기 (Path=/) 삭제
3335
response.addCookie(
3436
CookieUtils.deleteRefreshTokenCookie(cookieSecure)
3537
);
38+
39+
// 2. 레거시 쿠키(Path=/api/auth)도 삭제 (과거 잔재 청소)
40+
response.addCookie(CookieUtils.deleteRefreshTokenCookie(cookieSecure, "/api/auth", "Lax"));
41+
response.addCookie(CookieUtils.deleteRefreshTokenCookie(cookieSecure, "/api/auth", "Strict"));
3642
}
3743
}

src/main/java/com/ureca/unity/domain/auth/controller/OAuthController.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ public OAuthLoginResponse login(
3939
) {
4040
OAuthLoginResult result = oAuthService.login(OAuthProvider.from(provider), code);
4141

42+
// 1. 레거시(/api/auth) 쿠키 제거 (예전 SameSite가 Strict였다면 Strict로 한 번 더)
43+
response.addCookie(CookieUtils.deleteRefreshTokenCookie(cookieSecure, "/api/auth", "Strict"));
44+
response.addCookie(CookieUtils.deleteRefreshTokenCookie(cookieSecure, "/api/auth", "Lax"));
45+
46+
// 2. 정상(/) 쿠키 설정
4247
response.addCookie(
4348
CookieUtils.createRefreshTokenCookie(
4449
result.refreshToken(),

src/main/java/com/ureca/unity/domain/summary/controller/SummaryController.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,30 @@
33
import com.ureca.unity.domain.summary.dto.response.SummaryDetailResponse;
44
import com.ureca.unity.domain.summary.dto.response.SummaryListResponse;
55
import com.ureca.unity.domain.summary.service.SummaryService;
6+
import io.swagger.v3.oas.annotations.tags.Tag;
67
import lombok.RequiredArgsConstructor;
78
import org.springframework.web.bind.annotation.*;
89

910
import java.util.List;
1011

12+
@Tag(
13+
name = "4. Summary",
14+
description = "요약 관련 API"
15+
)
1116
@RestController
1217
@RequestMapping("/api/summaries")
1318
@RequiredArgsConstructor
1419
public class SummaryController {
1520

1621
private final SummaryService summaryService;
1722

18-
// 전체 요약 리스트
23+
// 전체 요약 목록
1924
@GetMapping
2025
public List<SummaryListResponse> getMySummaries(@RequestParam Long userId) {
2126
return summaryService.getMySummaries(userId);
2227
}
2328

24-
// 북마크 요약 리스트
29+
// 북마크 요약 목록
2530
@GetMapping("/bookmarks")
2631
public List<SummaryListResponse> getBookmarkedSummaries(@RequestParam Long userId) {
2732
return summaryService.getBookmarkedSummaries(userId);
@@ -32,4 +37,10 @@ public List<SummaryListResponse> getBookmarkedSummaries(@RequestParam Long userI
3237
public SummaryDetailResponse getSummaryDetail(@PathVariable Long summaryId) {
3338
return summaryService.getSummaryDetail(summaryId);
3439
}
40+
41+
// 북마크 토글 (프론트가 PATCH로 호출 중)
42+
@PatchMapping("/{summaryId}/bookmark")
43+
public void toggleBookmark(@PathVariable Long summaryId) {
44+
summaryService.toggleBookmark(summaryId);
45+
}
3546
}

src/main/java/com/ureca/unity/domain/summary/mapper/SummaryMapper.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,9 @@ void updateBookmark(
3939
@Param("isBookmarked") boolean isBookmarked
4040
);
4141

42-
// 전체 목록
4342
List<SummaryModel> findByUserId(@Param("userId") Long userId);
4443

45-
// 북마크 목록
4644
List<SummaryModel> findBookmarkedByUserId(@Param("userId") Long userId);
4745

48-
// 상세
4946
SummaryModel findById(@Param("summaryId") Long summaryId);
5047
}

src/main/java/com/ureca/unity/domain/summary/service/SummaryService.java

Lines changed: 27 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,7 @@ public class SummaryService {
2626
private final ObjectMapper objectMapper;
2727

2828
@Transactional
29-
public void createSummary(
30-
Long counselingResultId,
31-
Long userId,
32-
String counselingText
33-
) {
29+
public void createSummary(Long counselingResultId, Long userId, String counselingText) {
3430
if (userId == null) {
3531
throw new CustomException(ErrorCode.INVALID_INPUT_VALUE);
3632
}
@@ -61,13 +57,8 @@ public void createSummary(
6157

6258
summaryMapper.updateStatus(summaryId, "SUCCESS");
6359

64-
// 필요하면 반환형으로 바꾸거나, 로그용으로 사용
65-
new SummaryResponse(
66-
gemini.getTitle(),
67-
gemini.getSubject(),
68-
keywords,
69-
points
70-
);
60+
// (현재는 반환값 사용 안 하니 생성만 유지)
61+
new SummaryResponse(gemini.getTitle(), gemini.getSubject(), keywords, points);
7162

7263
} catch (Exception e) {
7364
summaryMapper.updateStatus(summaryId, "FAIL");
@@ -78,72 +69,31 @@ public void createSummary(
7869
@Transactional(readOnly = true)
7970
public List<SummaryListResponse> getMySummaries(Long userId) {
8071
return summaryMapper.findByUserId(userId).stream()
81-
.map(summary -> {
82-
try {
83-
List<String> keywords =
84-
summary.getKeywords() != null
85-
? objectMapper.readValue(summary.getKeywords(),
86-
new TypeReference<List<String>>() {})
87-
: List.of();
88-
89-
return new SummaryListResponse(
90-
summary.getSummaryId(),
91-
summary.getTitle(),
92-
summary.getStatus(),
93-
keywords,
94-
summary.getCreatedAt()
95-
);
96-
} catch (Exception e) {
97-
throw new IllegalStateException(e);
98-
}
99-
})
72+
.map(this::toListResponse)
10073
.toList();
10174
}
10275

10376
@Transactional(readOnly = true)
10477
public List<SummaryListResponse> getBookmarkedSummaries(Long userId) {
10578
return summaryMapper.findBookmarkedByUserId(userId).stream()
106-
.map(summary -> {
107-
try {
108-
List<String> keywords =
109-
summary.getKeywords() != null
110-
? objectMapper.readValue(summary.getKeywords(),
111-
new TypeReference<List<String>>() {})
112-
: List.of();
113-
114-
return new SummaryListResponse(
115-
summary.getSummaryId(),
116-
summary.getTitle(),
117-
summary.getStatus(),
118-
keywords,
119-
summary.getCreatedAt()
120-
);
121-
} catch (Exception e) {
122-
throw new IllegalStateException(e);
123-
}
124-
})
79+
.map(this::toListResponse)
12580
.toList();
12681
}
12782

12883
@Transactional(readOnly = true)
12984
public SummaryDetailResponse getSummaryDetail(Long summaryId) {
13085
SummaryModel summary = summaryMapper.findById(summaryId);
131-
132-
if (summary == null) {
133-
return null;
134-
}
86+
if (summary == null) return null;
13587

13688
try {
13789
List<String> keywords =
13890
summary.getKeywords() != null
139-
? objectMapper.readValue(summary.getKeywords(),
140-
new TypeReference<List<String>>() {})
91+
? objectMapper.readValue(summary.getKeywords(), new TypeReference<List<String>>() {})
14192
: List.of();
14293

14394
List<String> points =
14495
summary.getPoints() != null
145-
? objectMapper.readValue(summary.getPoints(),
146-
new TypeReference<List<String>>() {})
96+
? objectMapper.readValue(summary.getPoints(), new TypeReference<List<String>>() {})
14797
: List.of();
14898

14999
return new SummaryDetailResponse(
@@ -156,7 +106,6 @@ public SummaryDetailResponse getSummaryDetail(Long summaryId) {
156106
summary.getIsBookmarked(),
157107
summary.getCreatedAt()
158108
);
159-
160109
} catch (Exception e) {
161110
throw new IllegalStateException(e);
162111
}
@@ -165,11 +114,28 @@ public SummaryDetailResponse getSummaryDetail(Long summaryId) {
165114
@Transactional
166115
public void toggleBookmark(Long summaryId) {
167116
Boolean isBookmarked = summaryMapper.findBookmarkStatus(summaryId);
168-
169117
if (isBookmarked == null) {
170118
throw new IllegalArgumentException("summary not found");
171119
}
172-
173120
summaryMapper.updateBookmark(summaryId, !isBookmarked);
174121
}
122+
123+
private SummaryListResponse toListResponse(SummaryModel summary) {
124+
try {
125+
List<String> keywords =
126+
summary.getKeywords() != null
127+
? objectMapper.readValue(summary.getKeywords(), new TypeReference<List<String>>() {})
128+
: List.of();
129+
130+
return new SummaryListResponse(
131+
summary.getSummaryId(),
132+
summary.getTitle(),
133+
summary.getStatus(),
134+
keywords,
135+
summary.getCreatedAt()
136+
);
137+
} catch (Exception e) {
138+
throw new IllegalStateException(e);
139+
}
140+
}
175141
}

src/main/java/com/ureca/unity/global/config/WebMvcConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
1414
public void addCorsMappings(CorsRegistry registry) {
1515
registry.addMapping("/**") // 모든 엔드포인트
1616
.allowedOrigins(allowedOrigins.split("\\s*,\\s*"))
17-
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
17+
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
1818
.allowCredentials(true);
1919
}
2020
}

src/main/java/com/ureca/unity/global/util/CookieUtils.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public static Cookie createRefreshTokenCookie(
2121
cookie.setSecure(secure);
2222
cookie.setPath(PATH);
2323
cookie.setMaxAge((int) maxAgeSeconds);
24-
cookie.setAttribute("SameSite", "Strict");
24+
cookie.setAttribute("SameSite", "Lax");
2525
return cookie;
2626
}
2727

@@ -31,7 +31,23 @@ public static Cookie deleteRefreshTokenCookie(boolean secure) {
3131
cookie.setSecure(secure);
3232
cookie.setPath(PATH);
3333
cookie.setMaxAge(0);
34-
cookie.setAttribute("SameSite", "Strict");
34+
cookie.setAttribute("SameSite", "Lax");
3535
return cookie;
3636
}
37+
38+
public static Cookie deleteRefreshTokenCookie(boolean secure, String path, String sameSite) {
39+
String safePath = (path == null || path.isBlank()) ? PATH : path;
40+
41+
String normalizedSameSite =
42+
(sameSite != null && "Strict".equalsIgnoreCase(sameSite)) ? "Strict" : "Lax";
43+
44+
Cookie cookie = new Cookie(REFRESH_TOKEN, null);
45+
cookie.setHttpOnly(true);
46+
cookie.setSecure(secure);
47+
cookie.setPath(safePath);
48+
cookie.setMaxAge(0);
49+
cookie.setAttribute("SameSite", normalizedSameSite);
50+
return cookie;
51+
}
52+
3753
}

src/main/resources/mapper/summary/SummaryMapper.xml

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<mapper namespace="com.ureca.unity.domain.summary.mapper.SummaryMapper">
77

88
<!-- 요약 생성 -->
9-
<insert id="insertSummary" useGeneratedKeys="true" keyProperty="summaryId">
9+
<insert id="insertSummary">
1010
INSERT INTO summary (
1111
counseling_result_id,
1212
user_id,
@@ -37,8 +37,14 @@
3737
SET
3838
title = #{title},
3939
subject = #{subject},
40-
keywords = CAST(#{keywords} AS JSON),
41-
points = CAST(#{points} AS JSON)
40+
keywords = CASE
41+
WHEN #{keywords} IS NULL THEN NULL
42+
ELSE CAST(#{keywords} AS JSON)
43+
END,
44+
points = CASE
45+
WHEN #{points} IS NULL THEN NULL
46+
ELSE CAST(#{points} AS JSON)
47+
END
4248
WHERE summary_id = #{summaryId}
4349
</update>
4450

@@ -48,71 +54,73 @@
4854
WHERE summary_id = #{summaryId}
4955
</update>
5056

57+
<!-- 북마크 상태 -->
5158
<select id="findBookmarkStatus" resultType="boolean">
5259
SELECT is_bookmarked
5360
FROM summary
5461
WHERE summary_id = #{summaryId}
5562
</select>
5663

64+
<!-- 북마크 토글 -->
5765
<update id="updateBookmark">
5866
UPDATE summary
5967
SET is_bookmarked = #{isBookmarked}
6068
WHERE summary_id = #{summaryId}
6169
</update>
6270

63-
<!-- 전체 리스트 조회 -->
71+
<!-- 전체 리스트 -->
6472
<select id="findByUserId"
6573
resultType="com.ureca.unity.domain.summary.model.SummaryModel">
6674
SELECT
67-
summary_id AS summaryId,
68-
counseling_result_id AS counselingResultId,
69-
user_id AS userId,
75+
summary_id AS summaryId,
76+
counseling_result_id AS counselingResultId,
77+
user_id AS userId,
7078
title,
7179
subject,
7280
keywords,
7381
points,
74-
is_bookmarked AS isBookmarked,
82+
is_bookmarked AS isBookmarked,
7583
status,
76-
created_at AS createdAt
84+
created_at AS createdAt
7785
FROM summary
7886
WHERE user_id = #{userId}
7987
ORDER BY created_at DESC
8088
</select>
8189

82-
<!-- 북마크 리스트 조회 -->
90+
<!-- 북마크 리스트 -->
8391
<select id="findBookmarkedByUserId"
8492
resultType="com.ureca.unity.domain.summary.model.SummaryModel">
8593
SELECT
86-
summary_id AS summaryId,
87-
counseling_result_id AS counselingResultId,
88-
user_id AS userId,
94+
summary_id AS summaryId,
95+
counseling_result_id AS counselingResultId,
96+
user_id AS userId,
8997
title,
9098
subject,
9199
keywords,
92100
points,
93-
is_bookmarked AS isBookmarked,
101+
is_bookmarked AS isBookmarked,
94102
status,
95-
created_at AS createdAt
103+
created_at AS createdAt
96104
FROM summary
97105
WHERE user_id = #{userId}
98106
AND is_bookmarked = true
99107
ORDER BY created_at DESC
100108
</select>
101109

102-
<!-- 요약 상세 조회 -->
110+
<!-- 상세 -->
103111
<select id="findById"
104112
resultType="com.ureca.unity.domain.summary.model.SummaryModel">
105113
SELECT
106-
summary_id AS summaryId,
107-
counseling_result_id AS counselingResultId,
108-
user_id AS userId,
114+
summary_id AS summaryId,
115+
counseling_result_id AS counselingResultId,
116+
user_id AS userId,
109117
title,
110118
subject,
111119
keywords,
112120
points,
113-
is_bookmarked AS isBookmarked,
121+
is_bookmarked AS isBookmarked,
114122
status,
115-
created_at AS createdAt
123+
created_at AS createdAt
116124
FROM summary
117125
WHERE summary_id = #{summaryId}
118126
</select>

0 commit comments

Comments
 (0)