fix(backend): 계절별 영업시간을 날짜의 달로 골라 영업 중 표시를 바로잡음 #10

Merged
ttipo merged 1 commits from fix/seasonal-hours into main 2026-11-06 09:05:02 +09:00
Owner

무엇을 · 왜

하절기·동절기처럼 계절마다 시간표가 다른 곳을 어느 계절인지 모른 채 읽고 있었다. 개발 DB 1,117곳 중 30곳이 여기에 해당한다.

  • 23곳은 영업 중인지 판단하지 못했다. '영업시간 미확인'으로 나왔다(대구미술관·수창청춘맨숀·향촌문화관 등). 영업시간 미확인은 AI 코스가 문화시설·식당·카페를 후보에서 빼는 기준이기도 하다(설계 §3.3).
  • 7곳은 닫혀 있는 시각에도 영업 중으로 보였다. 두 계절의 시간을 한 주로 합쳐 읽었기 때문이다.
    • 대구수목원: 14월·912월에도 아침 8시에 영업 중으로 나왔다. 이 달들은 9시에 연다.
    • 달성 한일우호관: 11~2월 17시에도 영업 중으로 나왔다. 이 달들은 17시에 닫는다.
    • 상주식당: 쉬는날 칸에 "매년 12월~3월"이라고 적혀 있는데, 이 기간에도 영업 중으로 나왔다.

AI 코스 구현 2단계(후보 거르기·이동 시간·시간표·검증)는 날짜와 시각으로 영업을 확인한다. 그래서 그 전에 고친다.

바뀐 것 — backend/src/shared/utils/opening-hours.ts

  • 계절별 시간표(seasons): 아래 세 가지 표기를 계절로 읽는다.
    • 대괄호 머리말: [5월~8월]08:00~19:00
    • 줄 앞 라벨: 하절기(4월~10월) …, - 3월~10월 09:00~18:00
    • 시각 뒤 괄호: 10:00~23:00 (4월~11월)
  • isOpenAt(parsed, 'YYYY-MM-DD', 분): 요일 대신 날짜를 받아 그 달의 시간표를 고른다. 자정을 넘긴 영업은 전날의 달로 본다. kstClock 은 { date, minutes } 를 돌려준다. 장소 목록·상세의 openNow 는 그 날짜로 판단한다(place.service.ts).
  • 시기별 쉬는 요일(restSeasons): 시기마다 쉬는 요일이 다른 표기를 읽는다. 예를 들어 대구창의융합교육원은 37월·912월에는 월요일, 1·2·8월에는 일요일에 쉰다.
  • 문을 열지 않는 기간(closedPeriods): 쉬는날 칸에 기간만 적은 것("매년 12월3월")과 쉰다는 말이 붙은 기간("12월2월 휴장")을 읽는다.
  • 머리말 없는 시간: 계절 시간이 정하지 않은 요일을 채운다. 어느 계절도 확실히 덮지 않는 달에는 이 시간을 쓴다.
  • 다른 시설 머리말의 시간: 모든 계절에 합친다.
  • 기간으로 읽지 않는 것: 날짜("1월 1일")와 예외 문구 속의 달.
  • 기간을 읽지 못한 경우: 계절을 말하는데 기간을 읽지 못하면, 지금처럼 판단하지 않는다(null). 지금 개발 DB 에는 이런 곳이 없다.

달 없이 "하절기/동절기"만 적은 곳 (7곳)

하절기는 49월, 동절기는 122월만 확실하다고 본다. 대구 원문의 계절 범위가 기관마다 310월·49월·410월·411월 등으로 달라서다.

3·10·11월에는 두 시간표의 답이 같을 때만 판단한다. 예를 들어 수창청춘맨숀은 하절기 10:0019:00, 동절기 10:0018:00이다. 11월 14시에는 두 시간표 모두 열려 있어 영업 중으로 나온다. 18시 반에는 답이 갈려 미확인으로 나온다.

계절 경계가 달 중간에 있으면(예: 3월 15일부터) 그 달도 확실하지 않은 달로 둔다.

쉬는날 칸에 기간만 적은 곳

그 기간 내내 휴무로 읽는다. 강변축구장은 쉬는날 칸에 "동절기 (11월2월)"라고 적혀 있고, 영업시간 칸의 동절기는 103월이라 둘이 어긋난다. 이 경우 쉬는날 칸을 따라 11~2월을 휴장으로 본다.

이전 파서와 비교 — 개발 DB 1,117곳

2026년 각 달의 7일을 30분 간격으로 두 파서에 넣어 영업 중 판단을 비교했다.

바뀐 방향 장소
미확인 → 판단 23곳
영업 중 → 영업 종료 7곳: 달성 한일우호관 · 디아크문화관 · 대구수목원 · 사문진피크닉장 · 엄마아빠어렸을적에 · 상주식당 · 화본마을
영업 종료 → 영업 중, 판단 → 미확인 0곳

계절과 상관없는 곳은 해석 결과가 하나도 바뀌지 않았다.

남은 한계 (장소 DB 문서 §4.1 에 적었다)

  • 계절 안에 다시 시설 머리말이 있으면 미확인으로 둔다
  • 도학야영장의 예외 기간은 적용하지 않는다
  • 케이블카 출발 시각은 읽지 않는다

문서

  • docs/design/ai-course.md: §3.3 영업·영업시간 미확인 판단, §14 2단계 순서
  • docs/design/place-db.md: §4.1 계절별 시간표
  • docs/ai-eval/README.md: 계절별 시간표 항목

확인 방법

# 단위 테스트. '계절별 시간표' 묶음 15개(대구미술관·수창청춘맨숀·대구수목원·상주식당 등)
docker exec wandergo-backend npx jest src/shared/utils/opening-hours.spec.ts

# 저장된 해석 결과 다시 만들기(DB 만 쓰고 키는 필요 없다)
docker exec wandergo-backend npm run place:sync -- reparse-hours

개발 DB 는 이미 다시 해석해 두었다. 한 번 더 돌려도 바뀌는 곳은 0곳이다.

다른 DB 에 배포할 때는 reparse-hours 를 한 번 돌린다. 그 전에는 저장된 해석 결과가 예전 그대로라 영업 중 판단도 예전과 같다(위 30곳도 예전처럼 나온다).

영향 범위

  • backend
  • frontend
  • flutter
  • docker / 인프라
  • 규칙 문서(.claude/)

공유 계약은 바뀌지 않는다. 앱·웹이 받는 값(openNow: boolean | null, hours.text, restText, status)은 형식이 그대로이고, 값만 바로잡힌다. DB 의 해석 결과(place.public_hours)에 필드가 늘었지만 클라이언트에는 내려가지 않는다.

체크리스트

  • 린트/분석 통과 (Warning 0) · 변경 영역 테스트 통과: lint · tsc · 단위 262개 · e2e 48개(컨테이너)
  • 주석 한국어, 매직 넘버·하드코딩 문자열 없음
  • 사용자 노출 실패는 모두 에러 코드와 함께 처리: 해당 없음
  • 에러 코드를 바꿨다면 npm run sync:error-codes 실행 후 생성 파일까지 커밋: 해당 없음
  • 민감정보 미커밋
  • 로그에 비밀번호·토큰·요청 본문 전체가 남지 않음: 로그 변경 없음

🤖 Generated with Claude Code

## 무엇을 · 왜 하절기·동절기처럼 계절마다 시간표가 다른 곳을 어느 계절인지 모른 채 읽고 있었다. 개발 DB 1,117곳 중 30곳이 여기에 해당한다. - **23곳은 영업 중인지 판단하지 못했다.** '영업시간 미확인'으로 나왔다(대구미술관·수창청춘맨숀·향촌문화관 등). 영업시간 미확인은 AI 코스가 문화시설·식당·카페를 후보에서 빼는 기준이기도 하다(설계 §3.3). - **7곳은 닫혀 있는 시각에도 영업 중으로 보였다.** 두 계절의 시간을 한 주로 합쳐 읽었기 때문이다. - 대구수목원: 1~4월·9~12월에도 아침 8시에 영업 중으로 나왔다. 이 달들은 9시에 연다. - 달성 한일우호관: 11~2월 17시에도 영업 중으로 나왔다. 이 달들은 17시에 닫는다. - 상주식당: 쉬는날 칸에 "매년 12월~3월"이라고 적혀 있는데, 이 기간에도 영업 중으로 나왔다. AI 코스 구현 2단계(후보 거르기·이동 시간·시간표·검증)는 날짜와 시각으로 영업을 확인한다. 그래서 그 전에 고친다. ### 바뀐 것 — `backend/src/shared/utils/opening-hours.ts` - **계절별 시간표(`seasons`)**: 아래 세 가지 표기를 계절로 읽는다. - 대괄호 머리말: `[5월~8월]08:00~19:00` - 줄 앞 라벨: `하절기(4월~10월) …`, `- 3월~10월 09:00~18:00` - 시각 뒤 괄호: `10:00~23:00 (4월~11월)` - **`isOpenAt(parsed, 'YYYY-MM-DD', 분)`**: 요일 대신 날짜를 받아 그 달의 시간표를 고른다. 자정을 넘긴 영업은 전날의 달로 본다. `kstClock` 은 `{ date, minutes }` 를 돌려준다. 장소 목록·상세의 `openNow` 는 그 날짜로 판단한다(`place.service.ts`). - **시기별 쉬는 요일(`restSeasons`)**: 시기마다 쉬는 요일이 다른 표기를 읽는다. 예를 들어 대구창의융합교육원은 3~7월·9~12월에는 월요일, 1·2·8월에는 일요일에 쉰다. - **문을 열지 않는 기간(`closedPeriods`)**: 쉬는날 칸에 기간만 적은 것("매년 12월~3월")과 쉰다는 말이 붙은 기간("12월~2월 휴장")을 읽는다. - **머리말 없는 시간**: 계절 시간이 정하지 않은 요일을 채운다. 어느 계절도 확실히 덮지 않는 달에는 이 시간을 쓴다. - **다른 시설 머리말의 시간**: 모든 계절에 합친다. - **기간으로 읽지 않는 것**: 날짜("1월 1일")와 예외 문구 속의 달. - **기간을 읽지 못한 경우**: 계절을 말하는데 기간을 읽지 못하면, 지금처럼 판단하지 않는다(null). 지금 개발 DB 에는 이런 곳이 없다. #### 달 없이 "하절기/동절기"만 적은 곳 (7곳) 하절기는 4~9월, 동절기는 12~2월만 확실하다고 본다. 대구 원문의 계절 범위가 기관마다 3~10월·4~9월·4~10월·4~11월 등으로 달라서다. 3·10·11월에는 두 시간표의 답이 같을 때만 판단한다. 예를 들어 수창청춘맨숀은 하절기 10:00~19:00, 동절기 10:00~18:00이다. 11월 14시에는 두 시간표 모두 열려 있어 영업 중으로 나온다. 18시 반에는 답이 갈려 미확인으로 나온다. 계절 경계가 달 중간에 있으면(예: 3월 15일부터) 그 달도 확실하지 않은 달로 둔다. #### 쉬는날 칸에 기간만 적은 곳 그 기간 내내 휴무로 읽는다. 강변축구장은 쉬는날 칸에 "동절기 (11월~2월)"라고 적혀 있고, 영업시간 칸의 동절기는 10~3월이라 둘이 어긋난다. 이 경우 쉬는날 칸을 따라 11~2월을 휴장으로 본다. ### 이전 파서와 비교 — 개발 DB 1,117곳 2026년 각 달의 7일을 30분 간격으로 두 파서에 넣어 영업 중 판단을 비교했다. | 바뀐 방향 | 장소 | | --- | --- | | 미확인 → 판단 | 23곳 | | 영업 중 → 영업 종료 | 7곳: 달성 한일우호관 · 디아크문화관 · 대구수목원 · 사문진피크닉장 · 엄마아빠어렸을적에 · 상주식당 · 화본마을 | | 영업 종료 → 영업 중, 판단 → 미확인 | 0곳 | 계절과 상관없는 곳은 해석 결과가 하나도 바뀌지 않았다. ### 남은 한계 (장소 DB 문서 §4.1 에 적었다) - 계절 안에 다시 시설 머리말이 있으면 미확인으로 둔다 - 도학야영장의 예외 기간은 적용하지 않는다 - 케이블카 출발 시각은 읽지 않는다 ### 문서 - `docs/design/ai-course.md`: §3.3 영업·영업시간 미확인 판단, §14 2단계 순서 - `docs/design/place-db.md`: §4.1 계절별 시간표 - `docs/ai-eval/README.md`: 계절별 시간표 항목 ## 확인 방법 ```bash # 단위 테스트. '계절별 시간표' 묶음 15개(대구미술관·수창청춘맨숀·대구수목원·상주식당 등) docker exec wandergo-backend npx jest src/shared/utils/opening-hours.spec.ts # 저장된 해석 결과 다시 만들기(DB 만 쓰고 키는 필요 없다) docker exec wandergo-backend npm run place:sync -- reparse-hours ``` 개발 DB 는 이미 다시 해석해 두었다. 한 번 더 돌려도 바뀌는 곳은 0곳이다. **다른 DB 에 배포할 때**는 `reparse-hours` 를 한 번 돌린다. 그 전에는 저장된 해석 결과가 예전 그대로라 영업 중 판단도 예전과 같다(위 30곳도 예전처럼 나온다). ## 영향 범위 - [x] backend - [ ] frontend - [ ] flutter - [ ] docker / 인프라 - [ ] 규칙 문서(.claude/) > 공유 계약은 바뀌지 않는다. 앱·웹이 받는 값(`openNow`: boolean | null, `hours.text`, `restText`, `status`)은 형식이 그대로이고, 값만 바로잡힌다. DB 의 해석 결과(`place.public_hours`)에 필드가 늘었지만 클라이언트에는 내려가지 않는다. ## 체크리스트 - [x] 린트/분석 통과 (Warning 0) · 변경 영역 테스트 통과: lint · tsc · 단위 262개 · e2e 48개(컨테이너) - [x] 주석 한국어, 매직 넘버·하드코딩 문자열 없음 - [x] 사용자 노출 실패는 모두 에러 코드와 함께 처리: 해당 없음 - [x] 에러 코드를 바꿨다면 `npm run sync:error-codes` 실행 후 생성 파일까지 커밋: 해당 없음 - [x] 민감정보 미커밋 - [x] 로그에 비밀번호·토큰·요청 본문 전체가 남지 않음: 로그 변경 없음 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ttipo added 1 commit 2026-11-06 08:14:26 +09:00
fix(backend): 계절별 영업시간을 날짜의 달로 골라 영업 중 표시를 바로잡음
CI / Backend (lint · test · e2e · build) (pull_request) Successful in 12m4s
CI / Frontend (lint · type-check · test) (pull_request) Successful in 14m24s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 12m14s
CI / Backend (lint · test · e2e · build) (push) Successful in 12m25s
CI / Frontend (lint · type-check · test) (push) Successful in 11m3s
CI / 에러 코드 SSOT 동기화 검사 (push) Successful in 11m25s
f752bf28f5
하절기·동절기처럼 계절마다 시간표가 다른 곳을 어느 계절인지 모른 채 읽고 있었다.
23곳은 영업 중인지 판단하지 못해 '영업시간 미확인'으로 나왔고(대구미술관·수창청춘맨숀·향촌문화관 등),
7곳은 두 계절 시간을 합쳐 읽어 닫혀 있는 시각에도 영업 중으로 보였다
(대구수목원 9~4월 아침 8시, 달성 한일우호관 11~2월 17시, 상주식당 12~3월 휴업 등).
영업시간 미확인은 AI 코스가 문화시설·식당·카페를 후보에서 빼는 기준이기도 하다(설계 §3.3).

시간표를 계절별로 따로 두고(seasons) 그날 날짜의 달에 맞는 것을 고른다. isOpenAt 은 요일 대신
날짜를 받는다. 시기별 쉬는 요일(restSeasons)과 문을 열지 않는 기간(closedPeriods)도 따로 읽는다.
달 없이 "하절기/동절기"만 적은 곳은 하절기 4~9월·동절기 12~2월만 확실하다고 보고, 3·10·11월에는
두 시간표의 답이 같을 때만 판단한다. 대구 원문의 계절 범위가 기관마다 3~10월·4~11월 등으로 달라서다.

개발 DB 1,117곳을 이전 파서와 비교(2026년 달마다 7일 × 30분)해 30곳의 판단이 바뀌었다.
닫힘에서 영업 중으로, 판단에서 미확인으로 바뀐 곳은 없고, 계절과 상관없는 곳의 해석도 그대로다.
이전 형식으로 저장된 계절 구분 값은 reparse-hours 로 다시 해석하기 전까지 지금처럼 미확인이다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ttipo merged commit f752bf28f5 into main 2026-11-06 09:05:02 +09:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: ttipo/wandergo#10