Skip to content

add :: 급식 수동 refresh API 추가 - #657

Merged
1210thatman merged 5 commits into
developfrom
656-4월-급식-안뜸
Apr 2, 2026

Hidden character warning

The head ref may contain hidden characters: "656-4\uc6d4-\uae09\uc2dd-\uc548\ub738"
Merged

1210thatman merged 5 commits into
developfrom
656-4월-급식-안뜸

Conversation

@1210thatman

@1210thatman 1210thatman commented Apr 1, 2026 •

Copy link
Copy Markdown
Contributor

급식을 수동으로 refeash하는 API를 추가했습니다.

Summary by CodeRabbit

  • 새 기능
    • 관리자가 즉시 특정 월의 급식 데이터를 재수집·갱신할 수 있는 재시도 API(POST /admin/scheduler/meal/retry) 추가
    • 연월 단위로 한 달치 급식 데이터를 일괄 삭제하고 재저장하는 월별 삭제·갱신 기능 추가
  • 통합 및 외부 연동
    • 외부 급식 API의 월별 조회 엔드포인트와 관련 조회 흐름 확장
  • 데이터 무결성 / 모델 변경
    • 식사 데이터의 (mealDate, mealType)에 대한 DB 고유 제약 추가 및 mealType을 비 nullable로 변경

@1210thatman 1210thatman linked an issue Apr 1, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Apr 1, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8ad16dd4-b15e-42be-aa71-da8623644e66

📥 Commits

Reviewing files that changed from the base of the PR and between 3a4f582 and 5ac62f0.

📒 Files selected for processing (4)
  • src/main/kotlin/dsm/pick2024/domain/meal/domain/Meal.kt
  • src/main/kotlin/dsm/pick2024/domain/meal/entity/MealJpaEntity.kt
  • src/main/kotlin/dsm/pick2024/infrastructure/feign/neis/NeisMealFeignClientService.kt
  • src/main/resources/db/migration/V4_add_unique_constraint_to_meal_date_and_type.sql
✅ Files skipped from review due to trivial changes (1)
  • src/main/resources/db/migration/V4_add_unique_constraint_to_meal_date_and_type.sql
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/kotlin/dsm/pick2024/infrastructure/feign/neis/NeisMealFeignClientService.kt
  • src/main/kotlin/dsm/pick2024/domain/meal/entity/MealJpaEntity.kt

Walkthrough

현재 월의 기존 식사 데이터를 삭제하고 NEIS에서 동일 월의 식사 데이터를 재조회해 그룹·병합한 뒤 트랜잭션 내에서 삭제·저장하는 재시도 스케줄러 엔드포인트 및 관련 포트·서비스·퍼시스턴스·마이그레이션이 추가·수정되었습니다.

Changes

Cohort / File(s) Summary
컨트롤러 및 보안 경로
src/main/kotlin/dsm/pick2024/domain/admin/presentation/AdminController.kt, src/main/kotlin/dsm/pick2024/global/security/path/SecurityPaths.kt
RetryMealUseCase 주입 및 POST /admin/scheduler/meal/retry 엔드포인트 추가; 경로를 SCH_POST_ENDPOINTS에 등록.
유스케이스 및 서비스
src/main/kotlin/dsm/pick2024/domain/meal/port/in/RetryMealUseCase.kt, src/main/kotlin/dsm/pick2024/domain/meal/service/RetryMealService.kt
새 인터페이스 RetryMealUseCase 추가 및 RetryMealService 구현 추가. NEIS 월별 호출, 그룹화·병합 후 트랜잭션으로 기존 월 데이터 삭제 및 신규 저장 흐름 구현(동시성 제어 포함).
포트 확장
src/main/kotlin/dsm/pick2024/domain/meal/port/out/DeleteMealPort.kt, src/main/kotlin/dsm/pick2024/domain/meal/port/out/MealPort.kt
DeleteMealPort 추가 및 MealPort가 이를 상속하도록 확장(월 단위 삭제 계약 추가).
퍼시스턴스 레이어
src/main/kotlin/dsm/pick2024/domain/meal/persistence/MealPersistenceAdapter.kt, src/main/kotlin/dsm/pick2024/domain/meal/persistence/repository/MealRepository.kt
deleteByYearMonth(YearMonth) 구현 추가 및 JPA 리포지토리에 deleteAllByMealDateBetween(start,end) 선언 추가로 월 범위 삭제 위임.
FEIGN 클라이언트 리팩토링
src/main/kotlin/dsm/pick2024/infrastructure/feign/neis/NeisMealFeignClientService.kt
월 기반 조회 헬퍼 추가(getNeisInfoToEntityByYearMonth), 내부 호출 월 계산 방식 변경 및 getMealType이 알 수 없는 코드에 대해 예외를 던지도록 변경.
도메인·엔티티·마이그레이션
src/main/kotlin/dsm/pick2024/domain/meal/domain/Meal.kt, src/main/kotlin/dsm/pick2024/domain/meal/entity/MealJpaEntity.kt, src/main/resources/db/migration/V4_add_unique_constraint_to_meal_date_and_type.sql
도메인 Meal.mealType를 non-null로 변경, JPA 엔티티에 (mealDate, mealType) 유니크 제약 추가 및 해당 컬럼 NOT NULL 적용을 위한 DB 마이그레이션 추가.
사소한 정리
src/main/kotlin/dsm/pick2024/domain/meal/service/SaveMealService.kt
주석 정리(기능적 변경 없음).

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant AdminCtrl as AdminController
    participant RetrySvc as RetryMealService
    participant NeisFeign as NeisMealFeignClientService
    participant MealPort as MealPort
    participant Repo as MealRepository
    participant TxMgr as TransactionTemplate

    Client->>AdminCtrl: POST /admin/scheduler/meal/retry
    AdminCtrl->>RetrySvc: execute()
    RetrySvc->>RetrySvc: 현재 YearMonth 결정
    RetrySvc->>NeisFeign: getNeisInfoToEntityByYearMonth(yearMonth)
    NeisFeign-->>RetrySvc: 식사 데이터 목록
    RetrySvc->>RetrySvc: 날짜·타입별 그룹화 및 메뉴 병합
    RetrySvc->>TxMgr: 트랜잭션 시작
    RetrySvc->>MealPort: deleteByYearMonth(yearMonth)
    MealPort->>Repo: deleteAllByMealDateBetween(start, end)
    Repo-->>MealPort: 삭제 완료
    RetrySvc->>MealPort: save(Meal) 반복 호출
    MealPort->>Repo: 저장
    Repo-->>MealPort: 저장 완료
    TxMgr-->>RetrySvc: 트랜잭션 커밋
    RetrySvc-->>AdminCtrl: 완료
    AdminCtrl-->>Client: 200 OK
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 달빛에 당근 다듬듯 메뉴를 모아,
지운 달의 자리에 새 줄을 그려넣네.
NEIS 소식 모아 한 줄로 엮고,
트랜잭션 품에 포근히 저장하네.
토실토실 기쁜 리트라이 축하해!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 급식 수동 refresh API 추가라는 변경의 핵심을 명확하게 요약하고 있으며, 전체 PR의 주요 변경사항과 정확히 일치합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 656-4월-급식-안뜸

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/main/kotlin/dsm/pick2024/infrastructure/feign/neis/NeisMealFeignClientService.kt (1)

42-43: 날짜 포맷은 DateTimeFormatter.BASIC_ISO_DATE 직접 사용을 권장합니다.

Line [42]-Line [43]은 toString() 후 별도 변환 함수를 거치는데, Java Time 포맷터를 직접 쓰면 더 단순하고 안전합니다.

리팩터 예시
 import java.time.LocalDate
 import java.time.YearMonth
 import java.time.format.DateTimeFormatter
@@
 class NeisMealFeignClientService(
@@
 ) {
+    private val ymdFormatter: DateTimeFormatter = DateTimeFormatter.BASIC_ISO_DATE
@@
                 sdSchoolCode = NeisFeignClientRequestProperty.SD_SCHUL_CODE,
                 atptCode = NeisFeignClientRequestProperty.ATPT_OFCDC_CODE,
-                startedYmd = changeDateTimeFormat(yearMonth.atDay(1).toString()),
-                endedYmd = changeDateTimeFormat(yearMonth.atEndOfMonth().toString())
+                startedYmd = yearMonth.atDay(1).format(ymdFormatter),
+                endedYmd = yearMonth.atEndOfMonth().format(ymdFormatter)
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/kotlin/dsm/pick2024/infrastructure/feign/neis/NeisMealFeignClientService.kt`
around lines 42 - 43, The current code calls changeDateTimeFormat on
yearMonth.atDay(1).toString() and yearMonth.atEndOfMonth().toString(); instead,
format the LocalDate directly using DateTimeFormatter.BASIC_ISO_DATE to avoid
fragile toString parsing—replace the expressions used for startedYmd and
endedYmd with yearMonth.atDay(1).format(DateTimeFormatter.BASIC_ISO_DATE) and
yearMonth.atEndOfMonth().format(DateTimeFormatter.BASIC_ISO_DATE) (removing the
changeDateTimeFormat call) so the values are produced reliably from the
LocalDate instances.
src/main/kotlin/dsm/pick2024/domain/meal/persistence/repository/MealRepository.kt (1)

9-9: 삭제 건수 반환으로 운영 가시성을 높여주세요.

deleteAllByMealDateBetween 메서드를 Long을 반환하도록 변경하면 실제 삭제된 레코드 수를 추적할 수 있습니다. Spring Data JPA의 deleteAllBy* 메서드는 관례상 Long을 반환하며, 이를 통해 로그나 메트릭에 기록하면 장애 분석과 모니터링이 용이해집니다. 어댑터 레벨에서 반환값을 캡처하여 로깅하는 것도 고려해주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/kotlin/dsm/pick2024/domain/meal/persistence/repository/MealRepository.kt`
at line 9, 메서드 deleteAllByMealDateBetween의 반환형을 void에서 Long으로 변경하여 삭제된 레코드 수를
반환하도록 수정하세요: MealRepository 인터페이스의 fun deleteAllByMealDateBetween(start:
LocalDate, end: LocalDate): Long으로 시그니처를 바꾸고, 이 값을 호출하는 어댑터/서비스 레이어에서 (예:
MealRepository를 사용하는 클래스) 반환값을 받아 로깅/메트릭에 기록하도록 캡처 및 전파 처리하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/kotlin/dsm/pick2024/domain/meal/service/RetryMealService.kt`:
- Around line 31-33: The code in RetryMealService that computes cal via
mealsOfType.map { it.cal }.get(0) is non-deterministic when multiple cal values
exist; replace this with an explicit rule: collect distinct cal values from
mealsOfType (e.g., val cals = mealsOfType.map { it.cal }.distinct()) and then
either enforce a single value by calling single() and throw a clear exception
(including mealType/date context) if multiple are present, or apply a
deterministic aggregation rule (e.g., min/max/average) and document that choice;
update any surrounding logic that assumes a single cal accordingly.
- Around line 22-24: The current logic in RetryMealService
(mealPort.deleteByYearMonth followed by
neisMealFeignClientService.getNeisInfoToEntityByYearMonth) can wipe a month's
data if NEIS returns an empty list; change the sequence to first call
neisMealFeignClientService.getNeisInfoToEntityByYearMonth(currentMonth),
validate the result (non-null and non-empty) and only then call
mealPort.deleteByYearMonth(currentMonth) and persist the new data (save via the
existing save method), otherwise log/raise/skip without deleting; ensure this
runs inside the same transaction/context used by the service so the delete+save
remain atomic.
- Around line 18-24: The execute() method currently opens a `@Transactional`
context before calling
neisMealFeignClientService.getNeisInfoToEntityByYearMonth, which performs
external I/O; move the FEIGN call outside the transaction and only wrap the
database operations in a transaction. Concretely: call
YearMonth.from(TimeUtils.nowLocalDate()) and invoke
neisMealFeignClientService.getNeisInfoToEntityByYearMonth(currentMonth) first
(no `@Transactional`), then start a new transaction around
mealPort.deleteByYearMonth(currentMonth) and mealPort.save/saveAll (or whatever
persistence methods you use) so that only the DB write section runs in the
transactional scope; adjust or create a private `@Transactional` method (e.g.,
saveMealsTransactional) that accepts the fetched mealForSave and performs
mealPort.deleteByYearMonth and the save calls.

---

Nitpick comments:
In
`@src/main/kotlin/dsm/pick2024/domain/meal/persistence/repository/MealRepository.kt`:
- Line 9: 메서드 deleteAllByMealDateBetween의 반환형을 void에서 Long으로 변경하여 삭제된 레코드 수를
반환하도록 수정하세요: MealRepository 인터페이스의 fun deleteAllByMealDateBetween(start:
LocalDate, end: LocalDate): Long으로 시그니처를 바꾸고, 이 값을 호출하는 어댑터/서비스 레이어에서 (예:
MealRepository를 사용하는 클래스) 반환값을 받아 로깅/메트릭에 기록하도록 캡처 및 전파 처리하세요.

In
`@src/main/kotlin/dsm/pick2024/infrastructure/feign/neis/NeisMealFeignClientService.kt`:
- Around line 42-43: The current code calls changeDateTimeFormat on
yearMonth.atDay(1).toString() and yearMonth.atEndOfMonth().toString(); instead,
format the LocalDate directly using DateTimeFormatter.BASIC_ISO_DATE to avoid
fragile toString parsing—replace the expressions used for startedYmd and
endedYmd with yearMonth.atDay(1).format(DateTimeFormatter.BASIC_ISO_DATE) and
yearMonth.atEndOfMonth().format(DateTimeFormatter.BASIC_ISO_DATE) (removing the
changeDateTimeFormat call) so the values are produced reliably from the
LocalDate instances.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a92ffc6b-acfb-4719-b51d-a2516bb0faca

📥 Commits

Reviewing files that changed from the base of the PR and between cafe9a9 and 629d771.

📒 Files selected for processing (9)
  • src/main/kotlin/dsm/pick2024/domain/admin/presentation/AdminController.kt
  • src/main/kotlin/dsm/pick2024/domain/meal/persistence/MealPersistenceAdapter.kt
  • src/main/kotlin/dsm/pick2024/domain/meal/persistence/repository/MealRepository.kt
  • src/main/kotlin/dsm/pick2024/domain/meal/port/in/RetryMealUseCase.kt
  • src/main/kotlin/dsm/pick2024/domain/meal/port/out/DeleteMealPort.kt
  • src/main/kotlin/dsm/pick2024/domain/meal/port/out/MealPort.kt
  • src/main/kotlin/dsm/pick2024/domain/meal/service/RetryMealService.kt
  • src/main/kotlin/dsm/pick2024/global/security/path/SecurityPaths.kt
  • src/main/kotlin/dsm/pick2024/infrastructure/feign/neis/NeisMealFeignClientService.kt

Comment thread src/main/kotlin/dsm/pick2024/domain/meal/service/RetryMealService.kt Outdated
Comment on lines +22 to +24
mealPort.deleteByYearMonth(currentMonth)

val mealForSave = neisMealFeignClientService.getNeisInfoToEntityByYearMonth(currentMonth)

@coderabbitai coderabbitai Bot Apr 1, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

빈 응답 시 월 데이터가 통째로 유실될 수 있습니다.

현재 순서(삭제 → 외부 조회)는 NEIS가 일시적으로 빈 리스트를 반환해도 커밋 시 해당 월 데이터가 비는 결과를 만들 수 있습니다. 조회 결과를 먼저 검증한 뒤 삭제/저장을 수행하도록 순서를 바꾸는 게 안전합니다.

안전한 처리 순서 제안
 `@Transactional`
 override fun execute() {
     val currentMonth = YearMonth.from(TimeUtils.nowLocalDate())
-
-    mealPort.deleteByYearMonth(currentMonth)
-
-    val mealForSave = neisMealFeignClientService.getNeisInfoToEntityByYearMonth(currentMonth)
+    val mealForSave = neisMealFeignClientService.getNeisInfoToEntityByYearMonth(currentMonth)
+    require(mealForSave.isNotEmpty()) { "NEIS 응답이 비어 있어 급식 재동기화를 중단합니다." }
+
+    mealPort.deleteByYearMonth(currentMonth)
📝 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
mealPort.deleteByYearMonth(currentMonth)
val mealForSave = neisMealFeignClientService.getNeisInfoToEntityByYearMonth(currentMonth)
val mealForSave = neisMealFeignClientService.getNeisInfoToEntityByYearMonth(currentMonth)
require(mealForSave.isNotEmpty()) { "NEIS 응답이 비어 있어 급식 재동기화를 중단합니다." }
mealPort.deleteByYearMonth(currentMonth)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/kotlin/dsm/pick2024/domain/meal/service/RetryMealService.kt` around
lines 22 - 24, The current logic in RetryMealService (mealPort.deleteByYearMonth
followed by neisMealFeignClientService.getNeisInfoToEntityByYearMonth) can wipe
a month's data if NEIS returns an empty list; change the sequence to first call
neisMealFeignClientService.getNeisInfoToEntityByYearMonth(currentMonth),
validate the result (non-null and non-empty) and only then call
mealPort.deleteByYearMonth(currentMonth) and persist the new data (save via the
existing save method), otherwise log/raise/skip without deleting; ensure this
runs inside the same transaction/context used by the service so the delete+save
remain atomic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

이 부분은 오히려 도메인의 이해가 필요한 부분인 것 같아.
예를 들어 급식이 있는 날이었는데, 휴교 같은 이유로 인해서 급식이 안 나오는 날로 바뀔 수 도 있어.
그런데 빈 리스트가 왔다고 해서 변경하지 않는 건 오히려 잘못된 행동일 수 있는거지

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@1210thatman 좋은 도메인 설명 감사합니다. 말씀하신 것처럼 휴교·급식 미제공 등으로 인해 실제로 빈 리스트가 NEIS의 정상적인 응답일 수 있으므로, 빈 응답을 이유로 삭제를 막으면 오히려 DB에 오래된 데이터가 남아 잘못된 정보를 제공하게 되는군요. 현재의 "삭제 → 조회 → 저장" 순서가 도메인 의도에 맞는 올바른 구현임을 확인했습니다. 제 제안을 철회합니다.


📝 Learnings were identified and not saved (knowledge base disabled). Enable

Comment on lines +31 to +33
mealMap.forEach { (mealType, mealsOfType) ->
val menu = mealsOfType.map { it.menu }.filterNotNull().distinct().joinToString("||")
val cal = mealsOfType.map { it.cal }.get(0)

@coderabbitai coderabbitai Bot Apr 1, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

칼로리(cal) 선택 규칙이 비결정적입니다.

동일 날짜/식사타입 그룹에 cal 값이 여러 개면 첫 값만 저장되어 데이터 불일치가 숨겨질 수 있습니다. 단일 값 검증(불일치 시 예외) 또는 명시적 선택 규칙을 두는 것을 권장합니다.

검증 로직 예시
-    val cal = mealsOfType.map { it.cal }.get(0)
+    val calCandidates = mealsOfType.mapNotNull { it.cal }.distinct()
+    require(calCandidates.size <= 1) { "동일 mealDate/mealType에 cal 값이 여러 개입니다." }
+    val cal = calCandidates.firstOrNull()
📝 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
mealMap.forEach { (mealType, mealsOfType) ->
val menu = mealsOfType.map { it.menu }.filterNotNull().distinct().joinToString("||")
val cal = mealsOfType.map { it.cal }.get(0)
mealMap.forEach { (mealType, mealsOfType) ->
val menu = mealsOfType.map { it.menu }.filterNotNull().distinct().joinToString("||")
val calCandidates = mealsOfType.mapNotNull { it.cal }.distinct()
require(calCandidates.size <= 1) { "동일 mealDate/mealType에 cal 값이 여러 개입니다." }
val cal = calCandidates.firstOrNull()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/kotlin/dsm/pick2024/domain/meal/service/RetryMealService.kt` around
lines 31 - 33, The code in RetryMealService that computes cal via
mealsOfType.map { it.cal }.get(0) is non-deterministic when multiple cal values
exist; replace this with an explicit rule: collect distinct cal values from
mealsOfType (e.g., val cals = mealsOfType.map { it.cal }.distinct()) and then
either enforce a single value by calling single() and throw a clear exception
(including mealType/date context) if multiple are present, or apply a
deterministic aggregation rule (e.g., min/max/average) and document that choice;
update any surrounding logic that assumes a single cal accordingly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

이건 그냥 넘어가자 이 pr과는 관련 없는 내용이야

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@1210thatman 알겠습니다, 이 PR의 범위와 직접적인 관련이 없으니 넘어가겠습니다.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/kotlin/dsm/pick2024/domain/meal/service/RetryMealService.kt`:
- Around line 33-34: The multiline if/else in RetryMealService (the branch that
returns Meal(mealDate = date, mealType = mealType, menu = menu, cal = cal) else
null when menu.isNotEmpty()) lacks curly braces and causes ktlint to fail;
update that conditional to use explicit braces for both the if and the else so
the block is clearly multiline (i.e., wrap the true branch in { ... } and the
else branch in { ... }) within the function in RetryMealService.
- Around line 39-41: The refresh block inside RetryMealService.execute() is not
serialized and can interleave deleteByYearMonth/current-month reinsertions
causing duplicates or rollbacks; to fix, introduce a month-scoped lock around
the transactionTemplate.execute { mealPort.deleteByYearMonth(currentMonth);
mealsToSave.forEach { mealPort.save(it) } } (e.g., synchronized per-month key, a
distributed lock like `@SchedulerLock/ShedLock`, or a named ReentrantLock map
keyed by currentMonth) and/or enforce DB-level integrity by adding a unique
constraint on MealJpaEntity for (mealDate, mealType) and corresponding
repository handling in MealRepository to prevent duplicates on concurrent saves.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5405855b-7749-4780-bd9c-3f0c6dcb0b94

📥 Commits

Reviewing files that changed from the base of the PR and between 629d771 and ce81a2a.

📒 Files selected for processing (2)
  • src/main/kotlin/dsm/pick2024/domain/meal/service/RetryMealService.kt
  • src/main/kotlin/dsm/pick2024/domain/meal/service/SaveMealService.kt
✅ Files skipped from review due to trivial changes (1)
  • src/main/kotlin/dsm/pick2024/domain/meal/service/SaveMealService.kt

Comment thread src/main/kotlin/dsm/pick2024/domain/meal/service/RetryMealService.kt Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/kotlin/dsm/pick2024/domain/meal/entity/MealJpaEntity.kt`:
- Around line 14-18: The composite unique constraint (mealDate, mealType) is
ineffective because mealType is still nullable; update the V4 migration to alter
tbl_meal making mealType NOT NULL (e.g. ALTER TABLE ... MODIFY COLUMN `mealType`
TEXT NOT NULL), change the MealJpaEntity mapping for the mealType field to
nullable = false in its `@Column/`@Table mapping, and modify the getMealType()
method in MealJpaEntity to remove null returns by either throwing a clear
exception or returning/deriving a valid MealType default so the entity never
persists null mealType values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 04efe193-f098-4533-bf2b-d99e226fb003

📥 Commits

Reviewing files that changed from the base of the PR and between 9ce4f39 and 3a4f582.

📒 Files selected for processing (3)
  • src/main/kotlin/dsm/pick2024/domain/meal/entity/MealJpaEntity.kt
  • src/main/kotlin/dsm/pick2024/domain/meal/service/RetryMealService.kt
  • src/main/resources/db/migration/V4_add_unique_constraint_to_meal_date_and_type.sql
✅ Files skipped from review due to trivial changes (1)
  • src/main/resources/db/migration/V4_add_unique_constraint_to_meal_date_and_type.sql

Comment on lines +14 to +18
@Entity
@Table(
name = "tbl_meal",
uniqueConstraints = [UniqueConstraint(columnNames = ["mealDate", "mealType"])]
)

@coderabbitai coderabbitai Bot Apr 1, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Entity nullability check =="
rg -n -C2 'mealType|nullable = true|nullable = false' src/main/kotlin/dsm/pick2024/domain/meal/entity/MealJpaEntity.kt

echo
echo "== Migration nullability / column definition check =="
rg -n -C3 'tbl_meal|mealType|meal_type|NOT NULL|NULL|UNIQUE|uk_meal_date_type' src/main/resources/db/migration

echo
echo "== Upstream mapping where mealType can become nullable =="
rg -n -C3 'mealType' src/main/kotlin/dsm/pick2024/infrastructure/feign/neis src/main/kotlin/dsm/pick2024/domain/meal/service

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 12909


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== getMealType() function definition =="
rg -n -A10 'fun getMealType' src/main/kotlin/dsm/pick2024/infrastructure/feign/neis

echo
echo "== MealType enum definition =="
rg -n -B2 -A8 'enum class MealType|object MealType' src/main/kotlin/dsm/pick2024/domain/meal

echo
echo "== Check if migration altered column definition =="
cat src/main/resources/db/migration/V4_add_unique_constraint_to_meal_date_and_type.sql

Repository: DSM-PICK/PiCK_CORE_SERVER

Length of output: 1496


(mealDate, mealType) 유니크 제약이 mealType = NULL 케이스를 완전히 방지하지 못합니다.

Line 17의 복합 유니크 제약이 추가되었으나, Line 26-27에서 mealType이 nullable = true인 상태로 남아있습니다. SQL의 UNIQUE 제약은 NULL 값을 서로 비교하지 않으므로 (mealDate, NULL) 조합이 여러 번 삽입될 수 있어, 의도된 무결성 보장이 실패합니다. 마이그레이션 V4에서 제약만 추가하고 컬럼을 NOT NULL로 변경하지 않아 불완전합니다.

getMealType() (라인 129)이 알 수 없는 식사 코드에 대해 null을 반환할 수 있으므로, 컬럼을 NOT NULL로 수정하려면 동시에 getMealType() 오류 처리를 보완해야 합니다.

🔧 필요한 수정
  1. 마이그레이션에 NOT NULL 추가:
ALTER TABLE `tbl_meal` MODIFY COLUMN `mealType` TEXT NOT NULL;
  1. 엔티티 업데이트:
`@Enumerated`(value = EnumType.STRING)
-@Column(nullable = true, columnDefinition = "TEXT")
-val mealType: MealType?,
+@Column(nullable = false, columnDefinition = "TEXT")
+val mealType: MealType,
  1. getMealType() 예외 처리 추가 필요 (null 반환 제거)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/kotlin/dsm/pick2024/domain/meal/entity/MealJpaEntity.kt` around
lines 14 - 18, The composite unique constraint (mealDate, mealType) is
ineffective because mealType is still nullable; update the V4 migration to alter
tbl_meal making mealType NOT NULL (e.g. ALTER TABLE ... MODIFY COLUMN `mealType`
TEXT NOT NULL), change the MealJpaEntity mapping for the mealType field to
nullable = false in its `@Column/`@Table mapping, and modify the getMealType()
method in MealJpaEntity to remove null returns by either throwing a clear
exception or returning/deriving a valid MealType default so the entity never
persists null mealType values.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

NEIS API 스펙상, 1,2,3 이외의 값이 올 확률이 없다고 판단해서 저렇게 로직 작성했는데, 어떻게 생각해?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -A15 'fun getMealType' src/main/kotlin/dsm/pick2024/infrastructure/feign/neis

Length of output: 823


@1210thatman 맞는 말이에요 — NEIS API 스펙이 안정적이라면 실용적으로는 큰 문제가 없습니다. 다만 현재 구조에는 한 가지 아쉬운 점이 있어요.

현재 getMealType()의 else -> null 분기는 예상치 못한 값이 들어와도 조용히 null을 반환합니다. 만약 API 응답에서 예상 밖의 코드가 오더라도 아무런 에러 없이 mealType = null인 레코드가 저장될 수 있고, 그 경우 유니크 제약도 무력화됩니다.

// 현재
private fun getMealType(mealCode: String): MealType? = when (mealCode) {
    "1" -> MealType.BREAKFAST
    "2" -> MealType.LUNCH
    "3" -> MealType.DINNER
    else -> null  // 조용히 null 반환 → 추적 불가
}

"스펙상 1,2,3만 온다"는 확신이 있다면, 오히려 그 가정을 코드에 명시하는 게 더 좋습니다:

// 권장
private fun getMealType(mealCode: String): MealType = when (mealCode) {
    "1" -> MealType.BREAKFAST
    "2" -> MealType.LUNCH
    "3" -> MealType.DINNER
    else -> throw IllegalArgumentException("알 수 없는 NEIS 식사 코드: $mealCode")
}

이렇게 하면:

  • 반환 타입이 MealType(non-null) 이 되어, MealJpaEntity의 mealType도 자연스럽게 nullable = false로 변경할 수 있고,
  • 유니크 제약 (mealDate, mealType)이 의도대로 완전히 동작하며,
  • 혹시 API 스펙이 변경되거나 예외 케이스가 생겼을 때 즉시 인지할 수 있습니다.

결론적으로, 현재 로직이 틀렸다기보다는 가정이 코드에 드러나지 않아 나중에 추적하기 어렵다는 게 핵심입니다. 어떻게 생각하세요?

@1210thatman
1210thatman merged commit 34f50ca into develop Apr 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4월 급식 안뜸

2 participants