Skip to content

[Refactor] 장소 운영시간 데이터 이전 - #362

Open
ImHyungsuk wants to merge 19 commits into
developfrom
refactor/#341-normalize-operation-hours
Open

ImHyungsuk wants to merge 19 commits into
developfrom
refactor/#341-normalize-operation-hours

Conversation

@ImHyungsuk

@ImHyungsuk ImHyungsuk commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

🌳이슈 번호


resolves #341


☀️어떻게 이슈를 해결했나요?


  • 운영시간 테이블을 추가하고, 문자열로 작성되어있는 운영시간을 파싱해서 데이터를 저장했습니다.
  • '11:30 - 20:00\n브레이크 타임 16:00 - 17:00'의 경우 -> 11:30 - 16:00, 17:00 - 20:00로 저장

🗯️ PR 포인트

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요


  • 문자열에 오타들이 있어서 보이는건 다 수정했습니다.
  • 수정 최대한 했는데 나중에 유효하지 않은 값들 다시 찾아서 수정해야할 것 같습니다.
  • 다른 pr에서 flyway 버전이 14에서 17로 건너뛰어져 있는데, 운영에 크게 지장없고 미관상으로만 불편한거라 그 위로 18,19,20으로 버전 쌓아올렸습니다.

Summary by CodeRabbit

릴리스 노트

  • New Features
    • 영업시간 입력을 정기휴무, 브레이크타임, 라스트오더, 야간 영업(익일 종료)까지 포함해 더 정교하게 처리합니다.
  • Improvements
    • 기존 영업시간 데이터를 새로운 운영시간 슬롯 구조로 자동 정규화합니다.
  • Tests
    • 다양한 운영 시나리오에 대한 파싱 동작 검증 테스트를 추가했습니다.
  • Chores
    • 테스트 환경용 데이터베이스 설정 모드를 갱신했습니다.

@ImHyungsuk
ImHyungsuk requested review from 88guri and uykm April 18, 2026 08:41
@ImHyungsuk ImHyungsuk self-assigned this Apr 18, 2026
@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 2b400ef9-589d-4e92-b010-d1fb2a130044

📥 Commits

Reviewing files that changed from the base of the PR and between 44f2c2f and 11d068d.

📒 Files selected for processing (1)
  • src/main/java/org/sopt/solply_server/global/entity/DayOfWeekConverter.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/org/sopt/solply_server/global/entity/DayOfWeekConverter.java

Walkthrough

장소의 자유형 운영시간을 요일별 슬롯으로 파싱하고 operation_time_slots 테이블에 저장하도록 정규화했습니다. 스키마·입력 데이터 마이그레이션, Flyway 배치 이관, 파서 시나리오 테스트가 추가되었습니다.

Changes

운영시간 정규화 및 이관

Layer / File(s) Summary
스키마 및 입력 데이터 준비
src/main/java/org/sopt/solply_server/global/entity/*, src/main/java/org/sopt/solply_server/global/exception/ErrorCode.java, src/main/resources/db/migration/V18__fix_invalid_opening_hours_places.sql, src/main/resources/db/migration/V19__create_operation_hour_slot_from_place.sql
요일 값 변환과 오류 처리를 추가하고, 운영시간 슬롯 테이블 및 기존 입력 데이터 보정 마이그레이션을 정의합니다.
운영시간 텍스트 파싱 엔진
src/main/java/org/sopt/solply_server/domain/place/util/OperationHourParser.java
연중무휴, 정기휴무, 영업시간, 브레이크타임, 라스트오더, 익일 종료와 설명을 슬롯 목록으로 파싱합니다.
파싱 결과 배치 이관
src/main/java/db/migration/V20__Normalize_Operation_Hours.java
장소의 opening_hours를 파싱하고 휴무일·시간·설명·라스트오더 값을 매핑해 operation_time_slots에 배치 삽입합니다.
파서 시나리오 검증 및 테스트 환경
src/test/java/org/sopt/solply_server/domain/operation/util/OperationHourParserTest.java, src/test/resources/application-test.yml
요일 지정, 휴무, 브레이크타임, 라스트오더, 익일 종료, 비정형 설명 및 복합 스케줄을 검증하고 H2 모드를 MySQL로 변경합니다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Places as places
  participant Migration as V20__Normalize_Operation_Hours
  participant Parser as OperationHourParser
  participant Slots as operation_time_slots

  Migration->>Places: opening_hours 조회
  Places-->>Migration: 장소별 운영시간 반환
  Migration->>Parser: parse(rawText)
  Parser-->>Migration: 운영시간 슬롯 목록 반환
  Migration->>Slots: 슬롯 배치 삽입
Loading

Poem

깡충 토끼가 시간을 읽고
요일별 슬롯을 차곡차곡 담네
휴무와 쉼표도 놓치지 않고
라스트오더까지 살펴보며
새 테이블에 운영을 심었네 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 운영시간 데이터 이전이라는 핵심 변경을 간결하게 요약합니다.
Linked Issues check ✅ Passed 운영시간 테이블 분리, 기존 데이터 이관, 엔티티 생성, 관련 로직/쿼리 정규화 요구를 모두 충족합니다.
Out of Scope Changes check ✅ Passed 추가된 파일과 수정 내용은 운영시간 정규화와 데이터 이전 범위 안에 있으며 명백한 무관 변경은 보이지 않습니다.
✨ 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 refactor/#341-normalize-operation-hours

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.

@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: 9

🧹 Nitpick comments (4)
src/main/java/org/sopt/solply_server/domain/place/entity/OperationTimeSlot.java (1)

27-36: LAZY 연관관계는 toString에서 제외하세요.

@ToStringplace까지 출력하면 LAZY 로딩을 유발하거나 양방향 연관이 추가될 때 순환 출력 위험이 생깁니다. place를 제외하거나 엔티티의 @ToString을 제거해 주세요.

🧹 제안 수정
-@ToString
+@ToString(exclude = "place")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/sopt/solply_server/domain/place/entity/OperationTimeSlot.java`
around lines 27 - 36, The class-level `@ToString` on OperationTimeSlot currently
includes the LAZY-loaded field place which can trigger lazy loading or cause
recursive toString cycles; remove the risk by excluding place from the generated
toString (e.g., remove `@ToString` from the class or use Lombok exclusion such as
`@ToString.Exclude` on the place field or `@ToString`(exclude="place")) so the place
association is not printed.
src/main/java/db/migration/V17__Normalize_Operation_Hours.java (1)

15-16: Flyway 마이그레이션 로직을 고정된 구현으로 분리하는 것을 권장합니다.

V17이 애플리케이션의 OperationHourParser/DTO에 직접 의존하면, 나중에 파서가 리팩터링될 때 새 환경의 과거 마이그레이션 결과가 바뀌거나 깨질 수 있습니다. 마이그레이션 내부 전용 파서로 복사하거나 변경 불가능한 테스트를 붙여 주세요.

Also applies to: 31-32

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

In `@src/main/java/db/migration/V17__Normalize_Operation_Hours.java` around lines
15 - 16, The migration class V17__Normalize_Operation_Hours currently depends on
runtime classes OperationHourParser and OperationTimeDto; copy a self-contained,
immutable parser and DTO implementation into the migration file (e.g., private
static classes within V17__Normalize_Operation_Hours) and update the migration
to use those local symbols instead of org.sopt.solply_server.domain.* so the
migration is stable if those runtime classes change; also replicate the same
approach for the other referenced imports on lines 31-32 and add a small
migration-focused test that validates the parser behavior against known inputs
to lock the expected transformation.
src/main/java/org/sopt/solply_server/domain/place/util/OperationHourParser.java (1)

153-153: 파서 내부 디버그 로그 레벨을 낮춰주세요.

마이그레이션 중 모든 라인마다 INFO 로그가 남습니다. 운영 배포 로그를 오염시키지 않도록 제거하거나 debug로 낮추는 편이 좋습니다.

🧹 제안 수정
-		log.info("cleanLine:{}", cleanLine);
+		log.debug("cleanLine:{}", cleanLine);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/sopt/solply_server/domain/place/util/OperationHourParser.java`
at line 153, The parser currently logs every processed line at INFO level
(log.info("cleanLine:{}", cleanLine)) which floods production logs; change that
call to a lower level (e.g., log.debug("cleanLine:{}", cleanLine)) or remove it
from OperationHourParser so line-by-line parsing only emits debug-level
diagnostics.
src/main/java/org/sopt/solply_server/domain/place/dto/OperationTimeDto.java (1)

24-28: endTime 변경 시 endNextDay가 함께 갱신되도록 묶어주세요.

setEndTime()만 호출하면 endNextDay가 이전 값으로 남을 수 있습니다. 실제로 브레이크타임 분리 로직이 endTime을 바꾸므로, 두 필드를 한 메서드에서 함께 갱신하는 방식이 안전합니다.

🛠️ 제안 수정 방향
-	`@Setter`
 	private LocalTime endTime;
 
-	`@Setter`
 	private boolean endNextDay;
+
+	public void changeEndTime(LocalTime endTime) {
+		this.endTime = endTime;
+		this.endNextDay = startTime != null && !startTime.isBefore(endTime);
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/sopt/solply_server/domain/place/dto/OperationTimeDto.java`
around lines 24 - 28, OperationTimeDto currently uses Lombok-generated setters
for endTime and endNextDay which allows endTime to change without updating
endNextDay; remove the Lombok `@Setter` for endTime (and optionally endNextDay)
and implement a custom setEndTime method in OperationTimeDto that accepts the
new LocalTime (or new LocalTime plus boolean) and updates both the endTime and
endNextDay fields together (or recomputes endNextDay using the same logic your
break-time separation uses), ensuring any call to setEndTime(...) also sets
endNextDay consistently.
🤖 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/java/db/migration/V17__Normalize_Operation_Hours.java`:
- Around line 26-27: The query in V17__Normalize_Operation_Hours limits
migration by id (the SQL string in the try-with-resources that calls
executeQuery("SELECT id, opening_hours FROM places WHERE id <= 320")), which can
skip rows in production; change the SELECT to pick rows based on the original
column instead (e.g. WHERE opening_hours IS NOT NULL or WHERE opening_hours <>
'') so all rows with opening_hours are processed, and update the SQL string used
in the Statement in V17__Normalize_Operation_Hours accordingly.
- Around line 73-74: Remove the temporary per-place debug log: delete the
conditional block that checks placeId == 59 and the log.info call (the code
referencing placeId, log.info and slot) from V17__Normalize_Operation_Hours (the
migration class), so no per-place debug output remains in the migration; if any
logging is needed keep only a general, correctly formatted log (no hard-coded
placeId or malformed format string).

In
`@src/main/java/org/sopt/solply_server/domain/place/entity/OperationTimeSlot.java`:
- Around line 29-80: The factory methods createDayOn and createDayOff produce
entities missing required NOT NULL fields and timestamp inheritance; update the
class to extend BaseTimeEntity (to provide createdAt/updatedAt), make the place
field non-nullable, and change the factory signatures to accept a Place place
parameter and an endNextDay boolean (or default false) so createDayOn sets
s.place = place and s.endNextDay = endNextDay (and likewise createDayOff sets
s.place and s.endNextDay), ensuring OperationTimeSlot instances have place and
endNextDay populated before persisting.

In
`@src/main/java/org/sopt/solply_server/domain/place/util/OperationHourParser.java`:
- Around line 110-122: splitExistingSlots currently splits every active slot
into morning and afternoon pieces regardless of whether bStart/bEnd fall inside
that slot; change it to first check that bStart is after slot.getStartTime() and
bEnd is before the slot's originalEnd (taking into account next-day flags)
before mutating the slot. Only when the break interval lies strictly within the
existing slot create the morning piece by setting slot.setEndTime(bStart) and
also update the slot's endNextDay flag appropriately, then add the afternoon
piece via OperationTimeDto.createDayOn(slot.getDayOfWeek(), bEnd, originalEnd)
ensuring the new slot's endNextDay is set to match whether originalEnd crossed
midnight. Keep all other checks (days.contains(slot.getDayOfWeek()) &&
!slot.isDayOff()) but add the in-slot boundary checks and endNextDay updates to
prevent inverted or next-day ranges.
- Around line 125-135: The applyLastOrder method currently checks last-order
times by simple isAfter/isBefore against slot.getStartTime() and
slot.getEndTime(), which misses cases where endNextDay is true (overnight slots
like 18:00–02:00); update applyLastOrder (and use or add a time-in-range helper,
e.g., isTimeWithinSlotConsideringEndNextDay) to test each LocalTime lo against
the slot using startTime, endTime and slot.isEndNextDay() (or equivalent) so
that lo is considered inside the slot even when endNextDay is true; locate
OperationTimeDto getters (getStartTime, getEndTime, isDayOff, isEndNextDay) and
replace the current lo.isAfter/lo.isBefore checks with the helper call so
slot.setLastOrderTime(lo) runs for overnight ranges as well.
- Around line 179-188: In extractTimes, the current 24:XX handling drops minutes
by forcing any "24:" start to "00:00"; update the conversion so that "24:00"
becomes "00:00" but other "24:mm" keep the minute portion (e.g., "24:30" ->
"00:30") before parsing with LocalTime.parse (preserving the existing
zero-padding logic that prepends "0" when timeStr.length() == 4); ensure this
change is made inside the extractTimes method where the Matcher and timeStr are
handled.

In `@src/main/java/org/sopt/solply_server/global/entity/DayOfWeek.java`:
- Around line 12-18: Replace the throw of IllegalArgumentException in
DayOfWeek.from(int) with the project's BusinessException using an appropriate
ErrorCode; specifically, throw new
BusinessException(ErrorCode.INVALID_DAY_OF_WEEK) (add the INVALID_DAY_OF_WEEK
enum constant to ErrorCode if it doesn't exist) and update imports/usages
accordingly so the method consistently uses the project's
BusinessException/ErrorCode flow that integrates with GlobalExceptionHandler.

In `@src/main/java/org/sopt/solply_server/global/entity/DayOfWeekConverter.java`:
- Around line 9-15: The converter methods in DayOfWeekConverter are not
null-safe and will NPE when autoApply passes null; update
convertToDatabaseColumn(DayOfWeek dayOfWeek) to return null if dayOfWeek is null
(otherwise return dayOfWeek.getValue()) and update
convertToEntityAttribute(Integer integer) to return null if integer is null
(otherwise return DayOfWeek.from(integer)), so both conversions defensively
handle null inputs.

In
`@src/main/resources/db/migration/V16__create_operation_hour_slot_from_place.sql`:
- Line 21: The UNIQUE constraint uk_place_day currently on (place_id,
day_of_week, start_time) allows multiple NULL start_time rows (day-off slots);
update the schema so day-off rows are deduplicated by including is_day_off in
the uniqueness rule or by adding a generated column that normalizes NULL
start_time and using that in the UNIQUE key. Concretely, modify the constraint
referenced as uk_place_day (or create a new UNIQUE key) to include is_day_off
(e.g. UNIQUE(place_id, day_of_week, is_day_off, start_time) or use a generated
column like normalized_start_time = COALESCE(start_time, <sentinel>) and enforce
UNIQUE(place_id, day_of_week, is_day_off, normalized_start_time) so that rows
with start_time = NULL cannot be inserted multiple times for the same place/day.

---

Nitpick comments:
In `@src/main/java/db/migration/V17__Normalize_Operation_Hours.java`:
- Around line 15-16: The migration class V17__Normalize_Operation_Hours
currently depends on runtime classes OperationHourParser and OperationTimeDto;
copy a self-contained, immutable parser and DTO implementation into the
migration file (e.g., private static classes within
V17__Normalize_Operation_Hours) and update the migration to use those local
symbols instead of org.sopt.solply_server.domain.* so the migration is stable if
those runtime classes change; also replicate the same approach for the other
referenced imports on lines 31-32 and add a small migration-focused test that
validates the parser behavior against known inputs to lock the expected
transformation.

In `@src/main/java/org/sopt/solply_server/domain/place/dto/OperationTimeDto.java`:
- Around line 24-28: OperationTimeDto currently uses Lombok-generated setters
for endTime and endNextDay which allows endTime to change without updating
endNextDay; remove the Lombok `@Setter` for endTime (and optionally endNextDay)
and implement a custom setEndTime method in OperationTimeDto that accepts the
new LocalTime (or new LocalTime plus boolean) and updates both the endTime and
endNextDay fields together (or recomputes endNextDay using the same logic your
break-time separation uses), ensuring any call to setEndTime(...) also sets
endNextDay consistently.

In
`@src/main/java/org/sopt/solply_server/domain/place/entity/OperationTimeSlot.java`:
- Around line 27-36: The class-level `@ToString` on OperationTimeSlot currently
includes the LAZY-loaded field place which can trigger lazy loading or cause
recursive toString cycles; remove the risk by excluding place from the generated
toString (e.g., remove `@ToString` from the class or use Lombok exclusion such as
`@ToString.Exclude` on the place field or `@ToString`(exclude="place")) so the place
association is not printed.

In
`@src/main/java/org/sopt/solply_server/domain/place/util/OperationHourParser.java`:
- Line 153: The parser currently logs every processed line at INFO level
(log.info("cleanLine:{}", cleanLine)) which floods production logs; change that
call to a lower level (e.g., log.debug("cleanLine:{}", cleanLine)) or remove it
from OperationHourParser so line-by-line parsing only emits debug-level
diagnostics.
🪄 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.yml

Review profile: CHILL

Plan: Pro

Run ID: fd4770ea-e1f0-4fa3-88cc-7d81db10a4d8

📥 Commits

Reviewing files that changed from the base of the PR and between d6a6adb and 22ecc6e.

📒 Files selected for processing (8)
  • src/main/java/db/migration/V17__Normalize_Operation_Hours.java
  • src/main/java/org/sopt/solply_server/domain/place/dto/OperationTimeDto.java
  • src/main/java/org/sopt/solply_server/domain/place/entity/OperationTimeSlot.java
  • src/main/java/org/sopt/solply_server/domain/place/util/OperationHourParser.java
  • src/main/java/org/sopt/solply_server/global/entity/DayOfWeek.java
  • src/main/java/org/sopt/solply_server/global/entity/DayOfWeekConverter.java
  • src/main/resources/db/migration/V15__fix_invalid_opening_hours_places.sql
  • src/main/resources/db/migration/V16__create_operation_hour_slot_from_place.sql

Comment on lines +26 to +27
try (Statement select = connection.createStatement();
ResultSet rs = select.executeQuery("SELECT id, opening_hours FROM places WHERE id <= 320")) {

@coderabbitai coderabbitai Bot Apr 18, 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

마이그레이션 대상을 ID로 제한하지 마세요.

id <= 320은 운영 DB에 이미 더 큰 ID의 장소가 있으면 운영시간 정규화에서 누락시킵니다. 원본 컬럼 기준으로 대상 행을 고르는 쪽이 안전합니다.

🛠️ 제안 수정
 		try (Statement select = connection.createStatement();
-			 ResultSet rs = select.executeQuery("SELECT id, opening_hours FROM places WHERE id <= 320")) {
+			 ResultSet rs = select.executeQuery(
+				 "SELECT id, opening_hours FROM places WHERE opening_hours IS NOT NULL AND TRIM(opening_hours) <> ''"
+			 )) {
📝 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
try (Statement select = connection.createStatement();
ResultSet rs = select.executeQuery("SELECT id, opening_hours FROM places WHERE id <= 320")) {
try (Statement select = connection.createStatement();
ResultSet rs = select.executeQuery(
"SELECT id, opening_hours FROM places WHERE opening_hours IS NOT NULL AND TRIM(opening_hours) <> ''"
)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/db/migration/V17__Normalize_Operation_Hours.java` around lines
26 - 27, The query in V17__Normalize_Operation_Hours limits migration by id (the
SQL string in the try-with-resources that calls executeQuery("SELECT id,
opening_hours FROM places WHERE id <= 320")), which can skip rows in production;
change the SELECT to pick rows based on the original column instead (e.g. WHERE
opening_hours IS NOT NULL or WHERE opening_hours <> '') so all rows with
opening_hours are processed, and update the SQL string used in the Statement in
V17__Normalize_Operation_Hours 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.

의도했음

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@ImHyungsuk, 이해했습니다. id <= 320 제한이 초기 데이터셋에 해당하는 장소만을 대상으로 의도적으로 마이그레이션하는 것임을 확인했습니다. 제안을 철회하겠습니다.


✏️ Learnings added
Learnt from: ImHyungsuk
Repo: SOLPLY/SOLPLY-SERVER PR: 362
File: src/main/java/db/migration/V17__Normalize_Operation_Hours.java:26-27
Timestamp: 2026-04-18T09:28:56.268Z
Learning: In `src/main/java/db/migration/V17__Normalize_Operation_Hours.java`, the SQL query `WHERE id <= 320` is intentional. The migration is designed to only process the initial dataset of places (IDs up to 320) and is not meant to cover all rows with non-null opening_hours.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread src/main/java/db/migration/V17__Normalize_Operation_Hours.java Outdated
Comment thread src/main/java/org/sopt/solply_server/domain/place/entity/OperationTimeSlot.java Outdated
Comment on lines +110 to +122
private void splitExistingSlots(List<OperationTimeDto> result, List<Integer> days, LocalTime bStart,
LocalTime bEnd) {
List<OperationTimeDto> newSlots = new ArrayList<>();
Iterator<OperationTimeDto> it = result.iterator();
while (it.hasNext()) {
OperationTimeDto slot = it.next();
if (days.contains(slot.getDayOfWeek()) && !slot.isDayOff()) {
LocalTime originalEnd = slot.getEndTime();
slot.setEndTime(bStart); // 오전 슬롯으로 변경
newSlots.add(OperationTimeDto.createDayOn(slot.getDayOfWeek(), bEnd, originalEnd)); // 오후 슬롯 추가
}
}
result.addAll(newSlots);

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

브레이크타임이 실제 영업 구간 안에 있을 때만 슬롯을 분리하세요.

현재는 활성 요일의 모든 슬롯을 무조건 start~breakStart, breakEnd~originalEnd로 쪼갭니다. 브레이크타임이 해당 슬롯 밖에 있으면 17:00~15:00 같은 잘못된 구간이나 의도치 않은 익일 종료 슬롯이 생성될 수 있습니다.

분리 전에 breakStart/breakEnd가 원래 영업 구간 내부인지 검사하고, endTime 변경 후 endNextDay도 함께 갱신해 주세요.

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

In
`@src/main/java/org/sopt/solply_server/domain/place/util/OperationHourParser.java`
around lines 110 - 122, splitExistingSlots currently splits every active slot
into morning and afternoon pieces regardless of whether bStart/bEnd fall inside
that slot; change it to first check that bStart is after slot.getStartTime() and
bEnd is before the slot's originalEnd (taking into account next-day flags)
before mutating the slot. Only when the break interval lies strictly within the
existing slot create the morning piece by setting slot.setEndTime(bStart) and
also update the slot's endNextDay flag appropriately, then add the afternoon
piece via OperationTimeDto.createDayOn(slot.getDayOfWeek(), bEnd, originalEnd)
ensuring the new slot's endNextDay is set to match whether originalEnd crossed
midnight. Keep all other checks (days.contains(slot.getDayOfWeek()) &&
!slot.isDayOff()) but add the in-slot boundary checks and endNextDay updates to
prevent inverted or next-day ranges.

Comment thread src/main/java/org/sopt/solply_server/global/entity/DayOfWeek.java
CONSTRAINT fk_operation_time_place
FOREIGN KEY (place_id) REFERENCES places(id) ON DELETE CASCADE,

UNIQUE KEY uk_place_day (place_id, day_of_week, start_time)

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

🌐 Web query:

MySQL UNIQUE index multiple NULL values same indexed columns

💡 Result:

Yes, MySQL UNIQUE indexes permit multiple rows with NULL values in the indexed columns, even if all indexed columns are NULL. This applies to both single-column and multi-column (composite) UNIQUE indexes. This behavior is standard across MySQL versions, including the latest (8.4 as of 2026), and all storage engines except the deprecated BDB. NULL values are not considered equal to each other (NULL != NULL), so they do not violate uniqueness. Example: CREATE TABLE test (col1 INT NULL, col2 INT NULL, UNIQUE KEY uniq (col1, col2)); INSERT INTO test VALUES (NULL, NULL); -- Succeeds INSERT INTO test VALUES (NULL, NULL); -- Succeeds again INSERT INTO test VALUES (1, NULL); -- Succeeds INSERT INTO test VALUES (1, NULL); -- Fails: Duplicate entry '1-NULL' If you need to prevent multiple NULLs (e.g., treat NULL as a unique value), use a virtual generated column with COALESCE or IFNULL and index that: ALTER TABLE test ADD COLUMN col1_nonnull INT GENERATED ALWAYS AS (COALESCE(col1, -1)) VIRTUAL, ADD UNIQUE KEY uniq_nonnull (col1_nonnull, col2);

Citations:


🏁 Script executed:

# Find the migration files referenced in the review
find . -path "*db/migration*" -name "V16*" -o -name "V17*" | head -20

Repository: SOLPLY/SOLPLY-SERVER

Length of output: 209


🏁 Script executed:

# Check the actual content of V16 migration file
cat "src/main/resources/db/migration/V16__create_operation_hour_slot_from_place.sql"

Repository: SOLPLY/SOLPLY-SERVER

Length of output: 1169


🏁 Script executed:

# Look for V17 migration files (could be .sql or .java)
find . -path "*db/migration*" \( -name "V17*" -o -name "*V17*" \) | head -20

Repository: SOLPLY/SOLPLY-SERVER

Length of output: 128


🏁 Script executed:

# Check the content of V17 migration file
cat "src/main/java/db/migration/V17__Normalize_Operation_Hours.java"

Repository: SOLPLY/SOLPLY-SERVER

Length of output: 2947


🏁 Script executed:

# Search for code that inserts into operation_time_slots to see if there's duplicate prevention
rg -A 5 -B 5 "operation_time_slots" --type java | head -100

Repository: SOLPLY/SOLPLY-SERVER

Length of output: 2509


🏁 Script executed:

# Look for any other constraints or indexes on operation_time_slots
rg "uk_place_day|operation_time_slots" --type sql

Repository: SOLPLY/SOLPLY-SERVER

Length of output: 349


🏁 Script executed:

# Check if there's any service-layer logic preventing duplicate day-off insertions
rg -A 10 "isDayOff|is_day_off" --type java | head -150

Repository: SOLPLY/SOLPLY-SERVER

Length of output: 11056


🏁 Script executed:

# Check if there are any additional constraints or unique indexes beyond uk_place_day
rg "UNIQUE|INDEX" src/main/resources/db/migration/V16*.sql

Repository: SOLPLY/SOLPLY-SERVER

Length of output: 155


🏁 Script executed:

# Search for any application-level duplicate checks or updates after V17
rg -A 20 "operation_time_slots" src/main/resources/db/migration/ | grep -i "delete\|update\|constraint" | head -20

Repository: SOLPLY/SOLPLY-SERVER

Length of output: 541


중복 방지 제약 필요

현재 uk_place_day(place_id, day_of_week, start_time) 제약만으로는 휴무일 중복을 방지할 수 없습니다. V17에서 휴무 슬롯을 start_time = NULL로 삽입하는데, MySQL에서는 UNIQUE 제약이 NULL 값에 대해 중복을 허용하므로 같은 장소/요일의 휴무 행이 여러 개 생길 수 있습니다.

is_day_off 컬럼을 포함한 UNIQUE 제약, 생성 컬럼(generated column), 또는 서비스 계층 검증으로 보강해주세요.

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

In
`@src/main/resources/db/migration/V16__create_operation_hour_slot_from_place.sql`
at line 21, The UNIQUE constraint uk_place_day currently on (place_id,
day_of_week, start_time) allows multiple NULL start_time rows (day-off slots);
update the schema so day-off rows are deduplicated by including is_day_off in
the uniqueness rule or by adding a generated column that normalizes NULL
start_time and using that in the UNIQUE key. Concretely, modify the constraint
referenced as uk_place_day (or create a new UNIQUE key) to include is_day_off
(e.g. UNIQUE(place_id, day_of_week, is_day_off, start_time) or use a generated
column like normalized_start_time = COALESCE(start_time, <sentinel>) and enforce
UNIQUE(place_id, day_of_week, is_day_off, normalized_start_time) so that rows
with start_time = NULL cannot be inserted multiple times for the same place/day.

@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

♻️ Duplicate comments (2)
src/main/java/org/sopt/solply_server/domain/place/util/OperationHourParser.java (2)

125-138: ⚠️ Potential issue | 🟠 Major

자정 넘기는 슬롯의 라스트오더 판정이 여전히 불완전합니다. (부분 해결)

|| slot.isEndNextDay() 로 한쪽 조건은 보완됐지만, lo.isBefore(slot.getEndTime()) 조건은 그대로라서 18:00~02:00 슬롯에서 lo=22:00(자정 이전) 같은 값은 22:00.isBefore(02:00)false 이므로 여전히 누락됩니다. endNextDay를 고려한 포함 판정 헬퍼(예: isWithin(start, end, endNextDay, lo))를 도입해 양쪽 경계를 모두 처리해 주세요.

🛠️ 제안 수정 방향
-				for (LocalTime lo : loTimes) {
-					if (lo.isBefore(slot.getEndTime()) &&
-						(lo.isAfter(slot.getStartTime()) || slot.isEndNextDay())) {
-						slot.setLastOrderTime(lo);
-					}
-				}
+				for (LocalTime lo : loTimes) {
+					if (isWithinSlot(slot, lo)) {
+						slot.setLastOrderTime(lo);
+					}
+				}
private boolean isWithinSlot(OperationTimeDto slot, LocalTime t) {
    LocalTime s = slot.getStartTime();
    LocalTime e = slot.getEndTime();
    if (slot.isEndNextDay()) {
        // s <= t < 24:00  또는  00:00 <= t < e
        return !t.isBefore(s) || t.isBefore(e);
    }
    return !t.isBefore(s) && t.isBefore(e);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/sopt/solply_server/domain/place/util/OperationHourParser.java`
around lines 125 - 138, The last-order inclusion logic in applyLastOrder skips
times before midnight for slots that end the next day; introduce a helper (e.g.,
isWithinSlot(OperationTimeDto slot, LocalTime t)) that handles endNextDay
correctly (for endNextDay: return t >= start OR t < end; otherwise return start
<= t < end) and replace the current compound condition inside applyLastOrder
with a call to that helper before calling slot.setLastOrderTime(lo); reference
applyLastOrder, OperationTimeDto.isEndNextDay, and setLastOrderTime when
implementing the change.

110-123: ⚠️ Potential issue | 🟠 Major

브레이크타임이 슬롯 범위 밖일 때에도 여전히 무조건 쪼개집니다. (미해결)

이전 리뷰에서 지적된 내용이 반영되지 않았습니다. 현재 로직은 days.contains(...)!slot.isDayOff() 만 확인한 뒤 bStart/bEnd를 그대로 대입하므로, 브레이크 시간이 해당 슬롯의 [startTime, originalEnd] 밖에 있으면 17:00~15:00 같은 역전 구간이나 의도치 않은 익일 종료 슬롯이 만들어집니다. 또한 originalEnd가 자정을 넘어가는 경우(endNextDay=true) 포함 여부 판정이 단순 isBefore로는 맞지 않아 경계 검사도 endNextDay를 고려해야 합니다.

분리 전에 bStart가 슬롯 시작 이후이고 bEnd가 원래 종료 이전인지 확인하고, 쪼갠 뒤 앞 구간/뒤 구간 각각의 endNextDay도 재계산해 주세요.

🧹 Nitpick comments (3)
src/main/java/org/sopt/solply_server/domain/place/entity/OperationTimeSlot.java (1)

43-54: endNextDay를 primitive boolean으로 두는 편이 안전합니다.

DB 컬럼은 NOT NULL인데 필드는 Boolean 래퍼여서 @Setter 경로로 null이 세팅될 여지가 있습니다. isDayOff처럼 primitive로 바꾸면 default(false) 보장과 getter 네이밍(isEndNextDay())의 일관성도 함께 얻을 수 있습니다.

♻️ 제안 수정
-	`@Setter`
-	`@Column`
-	private Boolean endNextDay;
+	`@Setter`
+	`@Column`(nullable = false)
+	private boolean endNextDay;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/sopt/solply_server/domain/place/entity/OperationTimeSlot.java`
around lines 43 - 54, The field endNextDay is declared as the nullable wrapper
Boolean with `@Setter` which allows null to be set despite the DB column being NOT
NULL; change the field declaration to primitive boolean (like isDayOff) and keep
or adjust the `@Setter` so the generated setter/getter use primitive boolean
(resulting in isEndNextDay()), ensuring default false and avoiding null
assignments via the setter for the field endNextDay.
src/main/java/org/sopt/solply_server/domain/place/util/OperationHourParser.java (1)

76-87: 주석 처리된 죽은 코드와 디버그 로그 정리를 권장합니다.

  • Lines 76–87: parseOperationTime으로 이관 완료된 블록이 주석으로 남아 있습니다.
  • Line 154: log.info("cleanLine:{}", ...) 는 디버그용으로 보이는 흔적입니다. 마이그레이션 실행 시 매 줄마다 찍혀 노이즈가 큽니다.
  • Lines 192–196: 조건 분기가 죽은 주석으로 남아 있습니다.

삭제하거나 필요시 log.debug로 낮춰 주세요.

Also applies to: 154-154, 192-196

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

In
`@src/main/java/org/sopt/solply_server/domain/place/util/OperationHourParser.java`
around lines 76 - 87, The file contains dead commented code and noisy info-level
logging that should be cleaned up: remove the commented-out block that was
migrated to parseOperationTime (the fragment using parseDays, extractTimes,
OperationTimeDto.createDayOn and lastActiveDays) and drop the other commented
conditional branches left as dead-code in OperationHourParser; also change the
noisy log.info("cleanLine:{}" ...) to log.debug(...) or remove it if not needed
so migration runs don't log every line. Ensure you only delete/comment-clear the
obsolete snippets and preserve real logic in parseOperationTime and any live
conditionals.
src/main/java/org/sopt/solply_server/global/exception/ErrorCode.java (1)

125-125: 코드 컨벤션 불일치: DAY_001DAY-001 권장.

파일 내 대부분의 도메인 코드는 COMMON-001, AUTH-001, PLACE-001처럼 하이픈(-) + 3자리 숫자 형식을 따릅니다. 본 PR에서 새로 추가되는 INVALID_DAY_VALUE는 언더스코어를 사용하고 있어 컨벤션에서 벗어납니다(기존 PLACE_REVIEW_xxx도 동일한 문제가 있으나 본 PR 스코프는 아니므로 별건).

♻️ 제안 수정
-	INVALID_DAY_VALUE(HttpStatus.BAD_REQUEST, "DAY_001", "잘못된 요일 값입니다."),
+	INVALID_DAY_VALUE(HttpStatus.BAD_REQUEST, "DAY-001", "잘못된 요일 값입니다."),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/sopt/solply_server/global/exception/ErrorCode.java` at line
125, The new enum constant INVALID_DAY_VALUE in ErrorCode uses the code string
"DAY_001" which breaks the project's convention; change the code string to
"DAY-001" inside the ErrorCode enum (update the constructor argument for
INVALID_DAY_VALUE) and scan for any usages of ErrorCode.INVALID_DAY_VALUE to
ensure they still match the updated code string if they compare by string; leave
the enum constant name as-is but update only the error code literal to follow
the "XXX-###" convention.
🤖 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/java/org/sopt/solply_server/global/exception/ErrorCode.java`:
- Around line 34-35: The two enum entries EXPIRED_ACCESS_TOKEN and
EXPIRED_REFRESH_TOKEN currently share the same error code "AUTH-004", causing
clients to be unable to distinguish access vs refresh expiry; change
EXPIRED_ACCESS_TOKEN to keep "AUTH-004" and assign a distinct code to
EXPIRED_REFRESH_TOKEN (e.g., "AUTH-005") in ErrorCode so clients can handle
reissue vs re-login flows, and rename the DAY_001 entry to use the hyphenated
form "DAY-001" (and shift any following DAY codes if your numbering requires
reordering) so it matches the DOMAIN-NNN style used elsewhere.

---

Duplicate comments:
In
`@src/main/java/org/sopt/solply_server/domain/place/util/OperationHourParser.java`:
- Around line 125-138: The last-order inclusion logic in applyLastOrder skips
times before midnight for slots that end the next day; introduce a helper (e.g.,
isWithinSlot(OperationTimeDto slot, LocalTime t)) that handles endNextDay
correctly (for endNextDay: return t >= start OR t < end; otherwise return start
<= t < end) and replace the current compound condition inside applyLastOrder
with a call to that helper before calling slot.setLastOrderTime(lo); reference
applyLastOrder, OperationTimeDto.isEndNextDay, and setLastOrderTime when
implementing the change.

---

Nitpick comments:
In
`@src/main/java/org/sopt/solply_server/domain/place/entity/OperationTimeSlot.java`:
- Around line 43-54: The field endNextDay is declared as the nullable wrapper
Boolean with `@Setter` which allows null to be set despite the DB column being NOT
NULL; change the field declaration to primitive boolean (like isDayOff) and keep
or adjust the `@Setter` so the generated setter/getter use primitive boolean
(resulting in isEndNextDay()), ensuring default false and avoiding null
assignments via the setter for the field endNextDay.

In
`@src/main/java/org/sopt/solply_server/domain/place/util/OperationHourParser.java`:
- Around line 76-87: The file contains dead commented code and noisy info-level
logging that should be cleaned up: remove the commented-out block that was
migrated to parseOperationTime (the fragment using parseDays, extractTimes,
OperationTimeDto.createDayOn and lastActiveDays) and drop the other commented
conditional branches left as dead-code in OperationHourParser; also change the
noisy log.info("cleanLine:{}" ...) to log.debug(...) or remove it if not needed
so migration runs don't log every line. Ensure you only delete/comment-clear the
obsolete snippets and preserve real logic in parseOperationTime and any live
conditionals.

In `@src/main/java/org/sopt/solply_server/global/exception/ErrorCode.java`:
- Line 125: The new enum constant INVALID_DAY_VALUE in ErrorCode uses the code
string "DAY_001" which breaks the project's convention; change the code string
to "DAY-001" inside the ErrorCode enum (update the constructor argument for
INVALID_DAY_VALUE) and scan for any usages of ErrorCode.INVALID_DAY_VALUE to
ensure they still match the updated code string if they compare by string; leave
the enum constant name as-is but update only the error code literal to follow
the "XXX-###" convention.
🪄 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.yml

Review profile: CHILL

Plan: Pro

Run ID: f8b1c670-4980-49c4-99c8-a8c6e8742eb6

📥 Commits

Reviewing files that changed from the base of the PR and between 22ecc6e and 8f41fc5.

📒 Files selected for processing (5)
  • src/main/java/db/migration/V17__Normalize_Operation_Hours.java
  • src/main/java/org/sopt/solply_server/domain/place/entity/OperationTimeSlot.java
  • src/main/java/org/sopt/solply_server/domain/place/util/OperationHourParser.java
  • src/main/java/org/sopt/solply_server/global/entity/DayOfWeek.java
  • src/main/java/org/sopt/solply_server/global/exception/ErrorCode.java
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/org/sopt/solply_server/global/entity/DayOfWeek.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/db/migration/V17__Normalize_Operation_Hours.java

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor] 운영시간 테이블 정규화

1 participant