Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.pinback.api.test.controller;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.pinback.application.test.dto.request.PushTestRequest;
import com.pinback.application.test.dto.response.CategoriesTestResponse;
import com.pinback.application.test.port.in.TestPort;
import com.pinback.domain.user.entity.User;
import com.pinback.shared.annotation.CurrentUser;
import com.pinback.shared.dto.ResponseDto;

import io.swagger.v3.oas.annotations.Parameter;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/api/v1/test")
@RequiredArgsConstructor
public class TestController {
Comment on lines +21 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Gate “test” endpoints to non-prod (profile/feature flag or auth role)

These endpoints create/delete data and send push. Restrict to local/dev or admin.

-@RestController
-@RequestMapping("/api/v1/test")
-@RequiredArgsConstructor
-public class TestController {
+@org.springframework.context.annotation.Profile({"local","dev"})
+@org.springframework.validation.annotation.Validated
+@RestController
+@RequestMapping("/api/v1/test")
+@RequiredArgsConstructor
+public class TestController {

If profiles aren’t feasible, add @PreAuthorize("hasRole('ADMIN')") or guard via a property (e.g., @ConditionalOnProperty("pinback.test.enabled", havingValue="true")).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@RestController
@RequestMapping("/api/v1/test")
@RequiredArgsConstructor
public class TestController {
@org.springframework.context.annotation.Profile({"local","dev"})
@org.springframework.validation.annotation.Validated
@RestController
@RequestMapping("/api/v1/test")
@RequiredArgsConstructor
public class TestController {
🤖 Prompt for AI Agents
In api/src/main/java/com/pinback/api/test/controller/TestController.java around
lines 21 to 24, the controller exposing test endpoints is unguarded and must be
restricted to non-prod or admin use; update the class to be conditionally
enabled or authorized by either (a) adding a profile/conditional property such
as @Profile("local|dev") or @ConditionalOnProperty(name="pinback.test.enabled",
havingValue="true") to disable in prod, or (b) add method/class-level security
like @PreAuthorize("hasRole('ADMIN')") and ensure Spring Security configuration
enables global method security; apply one of these guards consistently to all
test endpoints and wire the corresponding property/profile or role so only
intended environments/users can access them.

private final TestPort testPort;

@PostMapping("/push")
public ResponseDto<Void> pushTest(@RequestBody PushTestRequest pushTestRequest) {
testPort.pushTest(pushTestRequest);
return ResponseDto.ok();
}
Comment on lines +27 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enable request validation on body

-	public ResponseDto<Void> pushTest(@RequestBody PushTestRequest pushTestRequest) {
+	public ResponseDto<Void> pushTest(@jakarta.validation.Valid @RequestBody PushTestRequest pushTestRequest) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@PostMapping("/push")
public ResponseDto<Void> pushTest(@RequestBody PushTestRequest pushTestRequest) {
testPort.pushTest(pushTestRequest);
return ResponseDto.ok();
}
@PostMapping("/push")
public ResponseDto<Void> pushTest(@jakarta.validation.Valid @RequestBody PushTestRequest pushTestRequest) {
testPort.pushTest(pushTestRequest);
return ResponseDto.ok();
}
🤖 Prompt for AI Agents
In api/src/main/java/com/pinback/api/test/controller/TestController.java around
lines 27 to 31, the request body is not validated; add request validation by
annotating the @RequestBody parameter with @Valid, ensure the controller class
is annotated with @Validated (or validation is enabled globally), and add
JSR-303 annotations (e.g., @NotNull, @NotBlank, @Size) to the fields in
PushTestRequest; also ensure a MethodArgumentNotValidException handler (or
global @ControllerAdvice) exists to return proper validation error responses.


@PostMapping("/articles")
public ResponseDto<Void> createArticles(
@Parameter(hidden = true) @CurrentUser User user,
@RequestParam Long categoryId
) {
testPort.createArticlesByCategory(user, categoryId);
return ResponseDto.ok();
}

@PostMapping("/categories")
public ResponseDto<CategoriesTestResponse> categoriesTest(
@Parameter(hidden = true) @CurrentUser User user
) {
CategoriesTestResponse response = testPort.createCategories(user);
return ResponseDto.ok(response);
}

@DeleteMapping("/articles/{categoryId}")
public ResponseDto<Void> deleteTest(
@Parameter(hidden = true) @CurrentUser User user,
@PathVariable Long categoryId
) {
testPort.deleteArticlesByCategory(user, categoryId);
return ResponseDto.ok();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.pinback.application.test.dto.request;

public record PushTestRequest(
String fcmToken,
String message
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.pinback.application.test.dto.response;

import java.util.List;

public record CategoriesTestResponse(
List<String> categories
) {
public static CategoriesTestResponse of(List<String> categories) {
return new CategoriesTestResponse(categories);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.pinback.application.test.port.in;

import com.pinback.application.test.dto.request.PushTestRequest;
import com.pinback.application.test.dto.response.CategoriesTestResponse;
import com.pinback.domain.user.entity.User;

public interface TestPort {
void pushTest(PushTestRequest request);

void createArticlesByCategory(User user, Long categoryId);

CategoriesTestResponse createCategories(User user);

void deleteArticlesByCategory(User user, Long categoryId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.pinback.application.test.port.out;

public interface FcmServicePort {
void sendNotification(String token, String message);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.pinback.application.test.usecase;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.UUID;

import org.springframework.stereotype.Service;

import com.pinback.application.article.port.out.ArticleDeleteServicePort;
import com.pinback.application.article.port.out.ArticleSaveServicePort;
import com.pinback.application.category.port.out.CategoryColorServicePort;
import com.pinback.application.category.port.out.CategoryGetServicePort;
import com.pinback.application.category.port.out.CategorySaveServicePort;
import com.pinback.application.test.dto.request.PushTestRequest;
import com.pinback.application.test.dto.response.CategoriesTestResponse;
import com.pinback.application.test.port.in.TestPort;
import com.pinback.application.test.port.out.FcmServicePort;
import com.pinback.application.user.port.out.UserGetServicePort;
import com.pinback.domain.article.entity.Article;
import com.pinback.domain.category.entity.Category;
import com.pinback.domain.category.enums.CategoryColor;
import com.pinback.domain.user.entity.User;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class TestUsecase implements TestPort {
private static final int CATEGORY_LIMIT = 10;
private final FcmServicePort fcmService;
private final CategoryGetServicePort categoryGetServicePort;
private final ArticleSaveServicePort articleSaveServicePort;
private final UserGetServicePort userGetServicePort;
private final CategorySaveServicePort categorySaveServicePort;
private final CategoryColorServicePort categoryColorServicePort;
private final ArticleDeleteServicePort articleDeleteServicePort;

@Override
public void pushTest(PushTestRequest request) {
fcmService.sendNotification(request.fcmToken(), request.message());
}

@Override
public void createArticlesByCategory(User user, Long categoryId) {
String format = "%s:%s";
Category category = categoryGetServicePort.findById(categoryId);

for (int i = 0; i < 5; i++) {
Article article = Article.create(
String.format(format, user.getEmail(), UUID.randomUUID()),
"testMemo",
user,
category,
null
);
articleSaveServicePort.save(article);
}
}

Comment on lines +45 to +61
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Wrap write operations in transactions

Prevents partial writes on mid-loop failures.

 	@Override
-	public void createArticlesByCategory(User user, Long categoryId) {
+	@org.springframework.transaction.annotation.Transactional
+	public void createArticlesByCategory(User user, Long categoryId) {

Also add (outside diff range) the import:

import org.springframework.transaction.annotation.Transactional;

I can annotate other write methods similarly in a follow-up diff below.

🤖 Prompt for AI Agents
In
application/src/main/java/com/pinback/application/test/usecase/TestUsecase.java
around lines 45 to 61, the createArticlesByCategory method performs multiple
writes without a transaction; annotate the method with @Transactional to ensure
the loop is executed within a single transaction so partial writes are rolled
back on failure, and add the import line import
org.springframework.transaction.annotation.Transactional; at the top of the
file; keep the annotation on the method signature and do not change method
visibility.

@Override
public CategoriesTestResponse createCategories(User user) {
User getUser = userGetServicePort.findById(user.getId());
List<String> defaultCategoryNames = Arrays.asList(
"집",
"취업",
"동아리",
"자기계발",
"포트폴리오",
"경제시사흐름",
"최신기술트렌드",
"인성직무면접꿀팁",
"어학자격증취득준비",
"멘탈관리스트레스해소"
);

List<String> createdCategoryNames = new ArrayList<>();
Set<CategoryColor> usedColors = categoryColorServicePort.getUsedColorsByUser(getUser);

for (int i = 0; i < 10; i++) {
String categoryName = defaultCategoryNames.get(i % defaultCategoryNames.size());
if (i >= defaultCategoryNames.size()) {
categoryName = categoryName + "_" + (i - defaultCategoryNames.size() + 1);
}

CategoryColor availableColor = getNextAvailableColor(usedColors);
Category category = Category.create(categoryName, getUser, availableColor);
Category savedCategory = categorySaveServicePort.save(category);
createdCategoryNames.add(savedCategory.getName());

// 사용된 색상 업데이트
usedColors.add(availableColor);
}

return CategoriesTestResponse.of(createdCategoryNames);
}

private CategoryColor getNextAvailableColor(Set<CategoryColor> usedColors) {
return Arrays.stream(CategoryColor.values())
.filter(color -> !usedColors.contains(color))
.findFirst()
.orElse(CategoryColor.COLOR1);
}

@Override
public void deleteArticlesByCategory(User user, Long categoryId) {
Category category = categoryGetServicePort.findById(categoryId);
User getUser = userGetServicePort.findById(user.getId());
articleDeleteServicePort.deleteByCategory(getUser, category.getId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public class Article extends BaseEntity {
@Column(name = "article_id")
private Long id;

@Column(name = "url", nullable = false)
@Column(name = "url", length = 700, nullable = false)
private String url;

@Column(name = "memo")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public RemindArticlesWithCount findTodayRemindWithCount(UUID userId, Pageable pa
.where(conditions)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(article.createdAt.desc())
.orderBy(article.remindAt.asc())
.fetch();
Comment on lines +158 to 159
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Switch to remindAt ASC is sensible, but add a deterministic tie-breaker for stable pagination.

Without a secondary key, rows sharing the same remindAt can shuffle between pages.

-            .orderBy(article.remindAt.asc())
+            .orderBy(article.remindAt.asc(), article.id.asc())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.orderBy(article.remindAt.asc())
.fetch();
.orderBy(article.remindAt.asc(), article.id.asc())
.fetch();
🤖 Prompt for AI Agents
in
infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java
around lines 158-159 the query currently orders only by remindAt which can lead
to non-deterministic result ordering for rows with identical remindAt; add a
deterministic tie-breaker by appending a secondary order by a unique, stable
column such as article.id (or another unique timestamp) so the orderBy becomes
remindAt.asc() followed by id.asc() to guarantee stable pagination across pages.


JPAQuery<Long> countQuery = queryFactory
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.pinback.infrastructure.firebase;

import org.springframework.stereotype.Component;

import com.pinback.application.test.port.out.FcmServicePort;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class FcmServiceAdapter implements FcmServicePort {
private final FcmService fcmService;

@Override
public void sendNotification(String token, String message) {
fcmService.sendNotification(token, message);
}
}