Skip to content

feat: 스케줄 추천 정책 통합 - #98

Merged
ehlung merged 16 commits into
developfrom
feature/scheduling-policy-integration
Jun 29, 2026
Merged

feat: 스케줄 추천 정책 통합#98
ehlung merged 16 commits into
developfrom
feature/scheduling-policy-integration

Conversation

@meteorqz6

@meteorqz6 meteorqz6 commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

요약

  • 최소 인원 조건 중심의 스케줄 추천 정책을 문서화했습니다.
  • 가능 시간 입력, 최소 인원 조건, 추천 생성 API에 정책 검증을 적용했습니다.
  • 가능 후보와 추천 배정을 분리해 저장하고 화면에서 구분해 표시했습니다.
  • 추천 재생성, 달력 표시, 30분 단위 시간 선택 UI를 정리했습니다.

검증

  • API 테스트 통과
  • web lint 통과
  • web typecheck 통과
  • web build 통과
  • commitlint 통과

Summary by CodeRabbit

  • New Features
    • 스케줄 추천 상세에 후보 목록(candidates)과 추천 여부(isRecommended)가 추가되었습니다.
    • 가용 시간 선택/저장은 최소 인원 규칙이 만족되는 구간으로 한정되도록 연동되었습니다.
  • Bug Fixes
    • 스태핑(업무/휴무/시간 경계) 관련 검증 및 가용성 정리가 더 정확히 동작하도록 개선되었습니다.
    • 최소 인원 규칙 변경·삭제 시 활성 기간의 가용 시간이 재정리되도록 수정했습니다.
  • UI Improvements
    • 가용 시간/최소 인원/스케줄 화면의 선택 범위와 안내 문구가 규칙 기반으로 업데이트되었습니다.

@vercel

vercel Bot commented Jun 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
final-project-web Ready Ready Preview, Comment Jun 29, 2026 4:56pm

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ehlung, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 11528928-12c7-45f7-8ccc-af134b86ea3b

📥 Commits

Reviewing files that changed from the base of the PR and between 123b0ad and f4e3732.

📒 Files selected for processing (1)
  • apps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsx

Walkthrough

최소 인원 조건을 영업시간과 교집합해 스케줄링 가능 구간을 계산하는 공용 정책 모듈을 추가하고, 가용 시간 저장·조회와 최소 인원 조건 변경 시 해당 구간 밖 데이터를 프루닝하도록 연결했습니다. 스케줄 추천은 수요 구간 기반으로 재구성되어 후보와 배정이 함께 저장·반환되며, 웹 화면과 문서 계약도 이에 맞게 갱신됐습니다.

Changes

최소 인원 조건 기반 스케줄링 정책

Layer / File(s) Summary
ScheduleCandidate 계약
packages/database/prisma/migrations/.../migration.sql, packages/database/prisma/schema.prisma, packages/shared/src/schemas/schedules.ts, packages/shared/src/types/api/index.ts, docs/ERD.md, docs/openapi.yaml
schedule_candidates 테이블, Prisma 관계, 공유 Zod/타입, ERD와 OpenAPI 스키마에 후보 데이터와 candidates 응답 필드를 추가한다.
스케줄링 가능 구간 계산
apps/api/src/modules/scheduling-time-policy.ts
영업시간과 최소 인원 규칙을 분 단위 범위로 변환해 교집합·병합·프루닝하는 공용 정책 함수와 타입을 추가한다.
최소 인원 조건 검증과 프루닝
apps/api/src/modules/staffing-rules/staffing-rules.service.ts, apps/api/src/modules/staffing-rules/staffing-rules.service.spec.ts, apps/api/test/staffing-rules.routes.test.ts
최소 인원 조건의 시작·종료 시각이 조직 영업시간 안인지 검증하고, 생성·수정·삭제 후 활성 기간 가용성을 다시 프루닝하도록 변경한다. 관련 거절 케이스와 삭제 후 프루닝 테스트를 추가한다.
가용 시간 프루닝
apps/api/src/modules/availability/availability.service.ts, apps/api/src/modules/availability/availability.service.spec.ts, apps/api/test/availability.routes.test.ts
가용 시간 조회 전 활성 기간을 먼저 정리하고, 저장 시 입력을 스케줄링 가능 구간으로 잘라서 저장한다. 해당 흐름을 검증하는 단위·통합 테스트가 갱신됐다.
추천 엔진과 스케줄 서비스
apps/api/src/modules/schedules/schedules.recommendation.ts, apps/api/src/modules/schedules/schedules.service.ts, apps/api/src/modules/schedules/schedules.mapper.ts, apps/api/src/modules/schedules/schedules.service.spec.ts, apps/api/test/helpers/fake-prisma.ts, apps/api/test/schedules.routes.test.ts
추천 로직이 날짜별 수요 구간과 후보 균형 기준으로 재구성되고, 스케줄 서비스는 프루닝 선행·후보 저장·최신 DRAFT 선택을 연결한다. fake-prisma와 서비스/라우트 테스트도 후보 포함 구조로 바뀌었다.
가용 시간과 최소 인원 조건 화면
apps/web/src/features/availability/components/mvp-availability-page.tsx, apps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsx, apps/web/src/features/staffing-rules/queries/staffing-rules-queries.ts, apps/web/src/components/ui/select.tsx, apps/web/src/components/common/password-input.tsx
가용 시간 화면은 최소 인원 규칙 기준으로 선택 가능한 슬롯만 표시하고, 최소 인원 조건 화면은 영업시간 기반 Select 입력과 클라이언트 검증으로 바뀐다. 정책 변경 시 관련 쿼리 무효화와 Select 높이 제한도 포함된다.
스케줄 페이지 API 통합
apps/web/src/features/schedules/api/schedules-api.ts, apps/web/src/features/schedules/queries/schedules-queries.ts, apps/web/src/features/schedules/queries/schedules-query-keys.ts, apps/web/src/features/schedules/components/mvp-schedules-page.tsx, apps/web/next.config.ts, apps/web/package.json
스케줄 API 클라이언트와 React Query 훅을 추가하고, MVP 스케줄 화면을 draft 조회·추천·배정 CRUD·확정·후보 표시 중심으로 API 기반 전환한다. 개발 서버 포트와 API 프록시 설정도 함께 갱신됐다.
제품 문서 갱신
docs/SCHEDULING_POLICY.md, docs/SPEC.md, docs/PRD.md, docs/API.md, docs/openapi.yaml, docs/ERD.md
최소 인원 조건 기반 가용 시간, 후보/추천 배정/미충족 정의, API 계약과 제품 범위를 문서에 반영한다.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning 요약/검증만 있고 템플릿의 개요·변경 사항·변경 유형·영향 범위·테스트 방법 등이 대부분 빠져 있습니다. 개요, 변경 사항, 변경 유형, 영향 범위, 테스트 방법, 체크리스트, 리뷰 포인트를 템플릿대로 채워 주세요.
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 (3 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 스케줄 추천 정책 통합이라는 핵심 변경과 잘 맞고, 변경 범위를 과도하게 넓히지 않습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/scheduling-policy-integration

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: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/modules/schedules/schedules.recommendation.ts (1)

277-305: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

입력 해시를 생성하기 전에 배열 순서를 정규화해 주세요.

현재 workers, availableTimes, businessHours, rules를 호출자가 넘긴 순서 그대로 직렬화합니다. DB 조회 orderBy가 누락되거나 바뀌면 같은 입력 조건도 다른 inputHash가 되어 DRAFT_ALREADY_EXISTS 중복 방지가 깨질 수 있습니다. 해시 내부에서 각 배열을 ID/요일/날짜/시간 기준으로 정렬해 canonical payload를 만드세요.

As per path instructions, “같은 입력 조건으로 이미 생성된 DRAFT가 있으면 중복 생성하지 않아야 함” 기준입니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/modules/schedules/schedules.recommendation.ts` around lines 277
- 305, The hash payload in schedules.recommendation should be canonicalized
before generating the input hash, because workers, availableTimes,
businessHours, and rules are currently serialized in caller-provided order.
Update the payload construction in the recommendation flow to sort each array
deterministically by stable fields such as id, workerId, dayOfWeek, date, and
time before mapping to the API shape, so the same logical input always produces
the same hash. Use the existing payload builder in schedules.recommendation to
keep DRAFT_ALREADY_EXISTS deduplication reliable even if upstream query ordering
changes.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/modules/availability/availability.service.ts`:
- Around line 238-250: Wrap the availability pruning delete/recreate flow in a
single transaction so it is atomic. In `availability.service.ts`, update the
logic around `client.workerAvailableTime.deleteMany` and
`client.workerAvailableTime.createMany` so `listAvailability` can run through
`PrismaClient.$transaction`, and when a `TransactionClient` is already passed
in, reuse that same transaction instead of starting a new one. Keep the
transactional boundary around the delete and conditional recreate steps in the
availability pruning helper/method.

In `@apps/api/src/modules/schedules/schedules.mapper.ts`:
- Around line 47-58: The candidate recommendation check in
isCandidateRecommended currently depends on workerId matching, so deleted
workers always map to isRecommended false. Update the matching logic in
schedules.mapper.ts to identify assignments by the preserved schedule
snapshot/time fields (for example workDate, startsAt, endsAt, and any stable
candidate/assignment identifiers available) rather than requiring non-null
workerId, so deleted candidates can still match their retained assignment
records. Ensure the comparison in isCandidateRecommended handles NULL workerId
cases consistently for both PrismaScheduleCandidate and
PrismaScheduleAssignment.

In `@apps/api/src/modules/schedules/schedules.service.ts`:
- Around line 156-178: `SchedulesService`의 추천 생성 경로에서
`pruneOrganizationAvailabilityForActivePlanningPeriod` 이후 다시 조회한
`availableTimes`가 비어 있는 경우에도 `createInputHash`와 `createRecommendations`로 진행되는
문제가 있습니다. `availableTimes`를 가져온 직후 빈 배열이면 추천 생성을 중단하고, 조직/인력/최소 인원 조건 검증과 함께 “선택
기간의 가능 시간 1개 이상” 계약을 `createRecommendations` 호출 전에 명시적으로 확인하도록
`schedules.service.ts`의 해당 분기 로직을 수정하세요.

In `@apps/api/src/modules/scheduling-time-policy.ts`:
- Line 1: The file has a Prettier formatting issue causing CI to fail, so apply
Prettier styling to the module and ensure the import block and surrounding
syntax match the project’s formatting rules. Use the existing module in
scheduling-time-policy and run the equivalent of prettier --write on it so the
formatting is normalized without changing behavior.
- Around line 136-144: The rule-range filtering in scheduling-time-policy
currently drops overnight business rules that wrap past midnight because
start/end are compared without normalization. Update the logic in the
scheduling-time-policy flow (the rules -> getRuleRange -> map/filter chain) so
that when a rule belongs to a closesNextDay business day and its start/end are
earlier than openTime, the minutes are normalized by adding 1440 before
intersecting with businessHourRange. Keep this behavior consistent with the
validation logic in staffing-rules.service so overnight ranges and the
zero-to-early-morning minimum staff window are preserved.

In `@apps/web/src/features/availability/components/mvp-availability-page.tsx`:
- Around line 210-218: The schedulability check in isSlotSchedulable only
verifies the slot start time, so a 30-minute slot can still extend past the
allowed range. Update the logic in mvp-availability-page.tsx so the full slot
interval is validated against getSchedulableRanges, using the slot end time as
well as the start time. Keep the fix localized to isSlotSchedulable and ensure
it only returns true when the entire 30-minute window fits within one
schedulable range.

In `@apps/web/src/features/schedules/components/mvp-schedules-page.tsx`:
- Around line 216-221: The schedule assignment flow in
createScheduleAssignmentInput and the time validation around the schedule form
currently assume endsAt is always on draft.date and only allow end times after
start times, which blocks overnight shifts. Update the scheduling logic so that
when the selected end time is earlier than the start time it is treated as
next-day 종료 and stored accordingly, while equal start/end times are still
rejected; adjust the validation near the time input handling and the input
builder to preserve overnight schedules.
- Around line 366-379: The schedule edit form still uses manual state and
validation instead of the shared React Hook Form + Zod approach, which
duplicates logic and can drift from API rules. Refactor MVPSchedulesPage to
remove the ad hoc draft/submitted/errors handling and wire the form through
React Hook Form, using the shared schema from packages/shared for validation.
Also update the field-level validation logic in the form section around the
schedule inputs to rely on the same shared Zod schema rather than web-only
checks, keeping the behavior aligned with ScheduleDraft and
validateScheduleDraft usage.
- Around line 353-360: `scheduleTargetDates` in `mvp-schedules-page.tsx` is too
narrow because it only includes dates present in `schedules`, `candidates`, or
`unfilledConditions`, which excludes empty workdays and blocks DRAFT add/view
flows. Update the `useMemo` for `scheduleTargetDates` so it is derived from the
full set of target work dates (including empty days) used by the schedule UI,
and make sure the downstream disabled state and detail-panel gating that rely on
`scheduleTargetDates` (around the day cell logic and detail panel selection)
continue to allow DRAFT add/edit/delete for those dates.

In `@apps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsx`:
- Around line 110-137: The createTimeOptions helper is generating start-boundary
options that display as 익일 but still save an unqualified time value, which later
gets rejected by validation in the staffing rules flow. Update createTimeOptions
and the related validation path around the MVP staffing rules page so that any
next-day start/end option is either excluded from start selections or encoded
with a stored value that preserves the day offset; make sure the logic used by
createTimeOptions, isSelectableBusinessHour, and the downstream check near the
validation block treats start/end boundaries, overnight hours, and 30-minute
steps consistently.

In `@docs/openapi.yaml`:
- Around line 1275-1321: The ScheduleCandidate schema is missing the digit-only
string constraint for its ID fields, so the OpenAPI contract is looser than the
shared Zod schema. Update the ScheduleCandidate properties for id and scheduleId
to use the same numeric-string pattern as idSchema, and keep workerId aligned
with the existing digit-only string convention already used elsewhere. Make the
OpenAPI definition consistent with the shared contract so generated clients and
validators enforce the same identifier format.

In `@docs/SPEC.md`:
- Around line 269-287: The scheduling spec currently allows recommendation
generation when there are no saved availability slots in the selected period,
but the implementation and PRD require at least one availability entry. Update
the recommendation-generation requirements in docs/SPEC.md to match the actual
behavior by making the “가능 시간 1개 이상” condition explicit, and adjust the “추천 생성
동작” flow around the availability-based candidate generation to reflect that
`DRAFT` creation only proceeds when saved availability exists, using the
scheduling policy terms and the recommendation generation section as the anchor
points.

---

Outside diff comments:
In `@apps/api/src/modules/schedules/schedules.recommendation.ts`:
- Around line 277-305: The hash payload in schedules.recommendation should be
canonicalized before generating the input hash, because workers, availableTimes,
businessHours, and rules are currently serialized in caller-provided order.
Update the payload construction in the recommendation flow to sort each array
deterministically by stable fields such as id, workerId, dayOfWeek, date, and
time before mapping to the API shape, so the same logical input always produces
the same hash. Use the existing payload builder in schedules.recommendation to
keep DRAFT_ALREADY_EXISTS deduplication reliable even if upstream query ordering
changes.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: f777087a-a388-44e5-b42b-a318dc019cf0

📥 Commits

Reviewing files that changed from the base of the PR and between 340dc0f and 9bd3ea3.

📒 Files selected for processing (31)
  • apps/api/src/modules/availability/availability.service.spec.ts
  • apps/api/src/modules/availability/availability.service.ts
  • apps/api/src/modules/schedules/schedules.mapper.ts
  • apps/api/src/modules/schedules/schedules.recommendation.ts
  • apps/api/src/modules/schedules/schedules.service.spec.ts
  • apps/api/src/modules/schedules/schedules.service.ts
  • apps/api/src/modules/scheduling-time-policy.ts
  • apps/api/src/modules/staffing-rules/staffing-rules.service.spec.ts
  • apps/api/src/modules/staffing-rules/staffing-rules.service.ts
  • apps/api/test/availability.routes.test.ts
  • apps/api/test/helpers/fake-prisma.ts
  • apps/api/test/schedules.routes.test.ts
  • apps/api/test/staffing-rules.routes.test.ts
  • apps/web/src/components/ui/select.tsx
  • apps/web/src/features/availability/components/mvp-availability-page.tsx
  • apps/web/src/features/schedules/api/schedules-api.ts
  • apps/web/src/features/schedules/components/mvp-schedules-page.tsx
  • apps/web/src/features/schedules/queries/schedules-queries.ts
  • apps/web/src/features/schedules/queries/schedules-query-keys.ts
  • apps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsx
  • apps/web/src/features/staffing-rules/queries/staffing-rules-queries.ts
  • docs/API.md
  • docs/ERD.md
  • docs/PRD.md
  • docs/SCHEDULING_POLICY.md
  • docs/SPEC.md
  • docs/openapi.yaml
  • packages/database/prisma/migrations/20260629000000_add_schedule_candidates/migration.sql
  • packages/database/prisma/schema.prisma
  • packages/shared/src/schemas/schedules.ts
  • packages/shared/src/types/api/index.ts

Comment thread apps/api/src/modules/availability/availability.service.ts Outdated
Comment thread apps/api/src/modules/schedules/schedules.mapper.ts
Comment on lines +156 to 178
await pruneOrganizationAvailabilityForActivePlanningPeriod(prisma, organizationDatabaseId);

const availableTimes = await prisma.workerAvailableTime.findMany({
where: {
worker: {
organizationId: organizationDatabaseId,
},
availableDate: {
gte: startDate,
lte: endDate,
},
},
orderBy: [{ availableDate: "asc" }, { startsAt: "asc" }],
});

const inputHash = createInputHash({
availableTimes,
businessHours: organization.businessHours,
endDate,
rules,
startDate,
workers,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

프루닝 후 가능 시간이 0건이면 추천 생성을 막아야 합니다.

Line 158에서 다시 조회한 availableTimes가 비어 있어도 그대로 inputHashcreateRecommendations로 넘어가서, 선택 기간에 가능 시간이 전혀 없는 경우에도 shortage-only DRAFT가 생성될 수 있습니다. 이 경로는 추천 생성 전에 가능 시간 1건 이상을 검증해야 한다는 계약과 어긋납니다.

수정 예시
   const availableTimes = await prisma.workerAvailableTime.findMany({
     where: {
       worker: {
         organizationId: organizationDatabaseId,
       },
       availableDate: {
         gte: startDate,
         lte: endDate,
       },
     },
     orderBy: [{ availableDate: "asc" }, { startsAt: "asc" }],
   });

+  if (availableTimes.length === 0) {
+    throw createValidationError("스케줄 추천에 필요한 가능 시간이 없습니다.");
+  }
+
   const inputHash = createInputHash({
     availableTimes,
     businessHours: organization.businessHours,

As per path instructions, "추천 생성 전 조직, 인력 1명 이상, 선택 기간의 가능 시간 1개 이상, 최소 인원 조건 1개 이상을 검증"해야 합니다.

📝 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
await pruneOrganizationAvailabilityForActivePlanningPeriod(prisma, organizationDatabaseId);
const availableTimes = await prisma.workerAvailableTime.findMany({
where: {
worker: {
organizationId: organizationDatabaseId,
},
availableDate: {
gte: startDate,
lte: endDate,
},
},
orderBy: [{ availableDate: "asc" }, { startsAt: "asc" }],
});
const inputHash = createInputHash({
availableTimes,
businessHours: organization.businessHours,
endDate,
rules,
startDate,
workers,
});
await pruneOrganizationAvailabilityForActivePlanningPeriod(prisma, organizationDatabaseId);
const availableTimes = await prisma.workerAvailableTime.findMany({
where: {
worker: {
organizationId: organizationDatabaseId,
},
availableDate: {
gte: startDate,
lte: endDate,
},
},
orderBy: [{ availableDate: "asc" }, { startsAt: "asc" }],
});
if (availableTimes.length === 0) {
throw createValidationError("스케줄 추천에 필요한 가능 시간이 없습니다.");
}
const inputHash = createInputHash({
availableTimes,
businessHours: organization.businessHours,
endDate,
rules,
startDate,
workers,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/modules/schedules/schedules.service.ts` around lines 156 - 178,
`SchedulesService`의 추천 생성 경로에서
`pruneOrganizationAvailabilityForActivePlanningPeriod` 이후 다시 조회한
`availableTimes`가 비어 있는 경우에도 `createInputHash`와 `createRecommendations`로 진행되는
문제가 있습니다. `availableTimes`를 가져온 직후 빈 배열이면 추천 생성을 중단하고, 조직/인력/최소 인원 조건 검증과 함께 “선택
기간의 가능 시간 1개 이상” 계약을 `createRecommendations` 호출 전에 명시적으로 확인하도록
`schedules.service.ts`의 해당 분기 로직을 수정하세요.

Source: Path instructions

Comment thread apps/api/src/modules/scheduling-time-policy.ts
Comment on lines +136 to +144
return rules
.filter((rule) => rule.dayOfWeek === getDayOfWeek(workDate))
.map(getRuleRange)
.map((ruleRange) => ({
endMinutes: Math.min(ruleRange.endMinutes, businessHourRange.endMinutes),
requiredCount: ruleRange.requiredCount,
startMinutes: Math.max(ruleRange.startMinutes, businessHourRange.startMinutes),
}))
.filter((range) => range.startMinutes < range.endMinutes);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

익일 영업일의 자정 이후 조건이 스케줄 가능 구간에서 누락됩니다.

예: 금요일 영업시간 22:00~02:00(closesNextDay)에서 금요일 기준 00:00~01:00 최소 인원 조건은 0~60분으로 계산되어 1320~1560 영업 구간과 교집합이 없어집니다. closesNextDay 영업일에서 start/endopenTime보다 작은 규칙은 +1440으로 정규화해야 합니다. 이 정규화는 staffing-rules.service.ts의 생성/수정 검증과도 동일하게 맞춰야 합니다.

As per path instructions, “날짜/시간 경계(익일 종료, 30분 단위, 휴무일)를 테스트하는지 확인” and “종료 시간이 시작 시간보다 빠르면 익일 종료로 처리” 기준입니다. Based on learnings, “가능 시간 관리(/availability)에서는 근무 기간, 조직 운영시간, 30분 단위, 휴무일 제외, 최소 인원 조건과의 교집합만 허용하는 규칙”을 따른 검토입니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/modules/scheduling-time-policy.ts` around lines 136 - 144, The
rule-range filtering in scheduling-time-policy currently drops overnight
business rules that wrap past midnight because start/end are compared without
normalization. Update the logic in the scheduling-time-policy flow (the rules ->
getRuleRange -> map/filter chain) so that when a rule belongs to a closesNextDay
business day and its start/end are earlier than openTime, the minutes are
normalized by adding 1440 before intersecting with businessHourRange. Keep this
behavior consistent with the validation logic in staffing-rules.service so
overnight ranges and the zero-to-early-morning minimum staff window are
preserved.

Sources: Path instructions, Learnings

Comment on lines +353 to +360
const scheduleTargetDates = useMemo(
() =>
new Set([
...schedules.map((item) => item.date),
...candidates.map((item) => item.date),
...unfilledConditions.map((condition) => condition.date),
]),
[candidates, schedules, unfilledConditions],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

빈 날짜에는 DRAFT 수동 추가를 할 수 없습니다.

scheduleTargetDates를 기존 assignment/candidate/shortage가 있는 날짜만으로 만들고 있어서, 아무 항목도 없는 근무일은 Line 800에서 비활성화되고 Line 873의 상세 패널도 열리지 않습니다. 그러면 추천 결과가 비어 있는 날짜에는 사용자가 DRAFT 스케줄을 직접 추가할 수 없어 핵심 조정 흐름이 깨집니다. As per path instructions, "DRAFT는 추가/수정/삭제 가능". Based on learnings, "Represent recommended schedules as DRAFT, allow users to add, modify, and delete day-level schedule entries."

🔧 수정 방향 예시
 const scheduleTargetDates = useMemo(
-  () =>
-    new Set([
-      ...schedules.map((item) => item.date),
-      ...candidates.map((item) => item.date),
-      ...unfilledConditions.map((condition) => condition.date),
-    ]),
-  [candidates, schedules, unfilledConditions],
+  () => {
+    if (schedule?.status === "DRAFT") {
+      return new Set(createDateRange(schedule.startDate, schedule.endDate));
+    }
+
+    return new Set([
+      ...schedules.map((item) => item.date),
+      ...candidates.map((item) => item.date),
+      ...unfilledConditions.map((condition) => condition.date),
+    ]);
+  },
+  [candidates, schedule, schedules, unfilledConditions],
 );

Also applies to: 800-818, 873-873

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/features/schedules/components/mvp-schedules-page.tsx` around
lines 353 - 360, `scheduleTargetDates` in `mvp-schedules-page.tsx` is too narrow
because it only includes dates present in `schedules`, `candidates`, or
`unfilledConditions`, which excludes empty workdays and blocks DRAFT add/view
flows. Update the `useMemo` for `scheduleTargetDates` so it is derived from the
full set of target work dates (including empty days) used by the schedule UI,
and make sure the downstream disabled state and detail-panel gating that rely on
`scheduleTargetDates` (around the day cell logic and detail panel selection)
continue to allow DRAFT add/edit/delete for those dates.

Sources: Path instructions, Learnings

Comment thread apps/web/src/features/schedules/components/mvp-schedules-page.tsx Outdated
Comment thread docs/openapi.yaml
Comment thread docs/SPEC.md
Comment on lines +269 to +287
정책 세부 기준은 `docs/SCHEDULING_POLICY.md`를 따릅니다.

- 조직이 있어야 합니다.
- 인력이 1명 이상 있어야 합니다.
- 선택 기간에 저장된 가능 시간이 1개 이상 있어야 합니다.
- 조직 영업시간이 설정되어 있어야 합니다.
- 최소 인원 조건이 1개 이상 있어야 합니다.
- 선택 기간에 저장된 가능 시간이 없으면 최소 인원 조건이 있는 전체 구간을 미충족 조건으로 기록할 수 있습니다.
- 같은 입력 조건으로 이미 생성된 DRAFT가 있으면 중복 생성하지 않습니다.

#### 추천 생성 동작

1. 사용자가 추천 생성을 요청합니다.
2. 서버는 인력, 가능 시간, 최소 인원 조건, 선택 기간을 기준으로 DRAFT 스케줄을 생성합니다.
3. 생성된 스케줄은 `DRAFT` 상태로 저장합니다.
4. 최소 인원 조건을 충족하지 못한 날짜와 시간대가 있으면 미충족 조건으로 기록합니다.
2. 서버는 조직 영업시간, 인력, 가능 시간, 최소 인원 조건, 선택 기간을 기준으로 DRAFT 스케줄을 생성합니다.
3. 최소 인원 조건이 있는 시간대에서만 가능한 후보를 산출합니다.
4. 가능 후보 중 추천 배정을 산출합니다.
5. 추천 배정은 최소 인원 조건을 최대한 충족한 뒤 주간 계약 시간 대비 배정 균형을 기준으로 선택합니다.
6. 모든 가능 후보의 계약 시간 사용률이 100% 이상이면 초과 폭이 가장 작은 근무자를 우선 선택하고 계약 시간 초과 경고 대상으로 표시합니다.
7. 생성된 스케줄은 `DRAFT` 상태로 저장합니다.
8. 최소 필요 인원 대비 추천 배정이 부족한 날짜와 시간대가 있으면 미충족 조건으로 기록합니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

가능 시간 0개 시 추천 생성 허용 여항과 구현 불일치

Line 275: "선택 기간에 저장된 가능 시간이 없으면 최소 인원 조건이 있는 전체 구간을 미충족 조건으로 기록할 수 있습니다"라고 기술되어 있으나, PRD 및 실제 구현의 추천 생성 조건에서는 "선택 기간에 저장된 가능 시간이 1개 이상 있어야 합니다"가 필수 조건입니다. SPEC이 구현보다 느슨하게 기술되어 있어 실제 API 동작과 혼동할 수 있습니다.

제안 수정:

- 선택 기간에 저장된 가능 시간이 없으면 최소 인원 조건이 있는 전체 구간을 미충족 조건으로 기록할 수 있습니다.
+ 선택 기간에 저장된 가능 시간이 1개 이상 있어야 합니다. 가능 시간이 없는 구간은 미충족 조건으로 기록됩니다.

또는 구현을 SPEC에 맞춰 가능 시간 0개 상태에서도 추천 생성을 허용하도록 변경해야 합니다. 어느 쪽이 의도된 동작인지 확인 후 일치시켜 주세요.

📝 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
정책 세부 기준은 `docs/SCHEDULING_POLICY.md`를 따릅니다.
- 조직이 있어야 합니다.
- 인력이 1명 이상 있어야 합니다.
- 선택 기간에 저장된 가능 시간이 1개 이상 있어야 합니다.
- 조직 영업시간이 설정되어 있어야 합니다.
- 최소 인원 조건이 1개 이상 있어야 합니다.
- 선택 기간에 저장된 가능 시간이 없으면 최소 인원 조건이 있는 전체 구간을 미충족 조건으로 기록할 수 있습니다.
- 같은 입력 조건으로 이미 생성된 DRAFT가 있으면 중복 생성하지 않습니다.
#### 추천 생성 동작
1. 사용자가 추천 생성을 요청합니다.
2. 서버는 인력, 가능 시간, 최소 인원 조건, 선택 기간을 기준으로 DRAFT 스케줄을 생성합니다.
3. 생성된 스케줄은 `DRAFT` 상태로 저장합니다.
4. 최소 인원 조건을 충족하지 못한 날짜와 시간대가 있으면 미충족 조건으로 기록합니다.
2. 서버는 조직 영업시간, 인력, 가능 시간, 최소 인원 조건, 선택 기간을 기준으로 DRAFT 스케줄을 생성합니다.
3. 최소 인원 조건이 있는 시간대에서만 가능한 후보를 산출합니다.
4. 가능 후보 중 추천 배정을 산출합니다.
5. 추천 배정은 최소 인원 조건을 최대한 충족한 뒤 주간 계약 시간 대비 배정 균형을 기준으로 선택합니다.
6. 모든 가능 후보의 계약 시간 사용률이 100% 이상이면 초과 폭이 가장 작은 근무자를 우선 선택하고 계약 시간 초과 경고 대상으로 표시합니다.
7. 생성된 스케줄은 `DRAFT` 상태로 저장합니다.
8. 최소 필요 인원 대비 추천 배정이 부족한 날짜와 시간대가 있으면 미충족 조건으로 기록합니다.
정책 세부 기준은 `docs/SCHEDULING_POLICY.md`를 따릅니다.
- 조직이 있어야 합니다.
- 인력이 1명 이상 있어야 합니다.
- 조직 영업시간이 설정되어 있어야 합니다.
- 최소 인원 조건이 1개 이상 있어야 합니다.
- 선택 기간에 저장된 가능 시간이 1개 이상 있어야 합니다. 가능 시간이 없는 구간은 미충족 조건으로 기록됩니다.
- 같은 입력 조건으로 이미 생성된 DRAFT가 있으면 중복 생성하지 않습니다.
#### 추천 생성 동작
1. 사용자가 추천 생성을 요청합니다.
2. 서버는 조직 영업시간, 인력, 가능 시간, 최소 인원 조건, 선택 기간을 기준으로 DRAFT 스케줄을 생성합니다.
3. 최소 인원 조건이 있는 시간대에서만 가능한 후보를 산출합니다.
4. 가능 후보 중 추천 배정을 산출합니다.
5. 추천 배정은 최소 인원 조건을 최대한 충족한 뒤 주간 계약 시간 대비 배정 균형을 기준으로 선택합니다.
6. 모든 가능 후보의 계약 시간 사용률이 100% 이상이면 초과 폭이 가장 작은 근무자를 우선 선택하고 계약 시간 초과 경고 대상으로 표시합니다.
7. 생성된 스케줄은 `DRAFT` 상태로 저장합니다.
8. 최소 필요 인원 대비 추천 배정이 부족한 날짜와 시간대가 있으면 미충족 조건으로 기록됩니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/SPEC.md` around lines 269 - 287, The scheduling spec currently allows
recommendation generation when there are no saved availability slots in the
selected period, but the implementation and PRD require at least one
availability entry. Update the recommendation-generation requirements in
docs/SPEC.md to match the actual behavior by making the “가능 시간 1개 이상” condition
explicit, and adjust the “추천 생성 동작” flow around the availability-based candidate
generation to reflect that `DRAFT` creation only proceeds when saved
availability exists, using the scheduling policy terms and the recommendation
generation section as the anchor points.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/web/src/features/schedules/components/mvp-schedules-page.tsx (2)

416-422: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

근무자 0명일 때 추천 생성을 막아 주세요.

canGenerate가 최소 인원 조건만 확인해 근무자가 없어도 추천 생성 버튼이 활성화됩니다. 서버 오류로 보내기 전에 버튼 조건과 안내 문구를 맞춰 주세요. As per path instructions, "추천 생성 전 조직, 인력 1명 이상, 선택 기간의 가능 시간 1개 이상, 최소 인원 조건 1개 이상을 검증".

수정 예시
   const canGenerate =
     Boolean(activePlanningPeriod) &&
+    workers.length > 0 &&
     staffingRules.length > 0 &&
     (!schedule || isDraft) &&
+              {workers.length === 0 ? (
+                <p className="mt-2 text-sm text-destructive">
+                  근무자를 먼저 등록해야 추천 스케줄을 생성할 수 있습니다.
+                </p>
+              ) : null}
               {staffingRules.length === 0 ? (

Also applies to: 719-723

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/features/schedules/components/mvp-schedules-page.tsx` around
lines 416 - 422, `canGenerate` in `mvp-schedules-page.tsx` only checks the
minimum staffing rules, so the recommendation action can still be enabled when
there are zero workers. Update the generation guard and any related
button/message logic to also require at least one worker, alongside the existing
checks in `canGenerate` and the schedule generation flow, so the UI matches the
server-side validation before invoking the recommendation request.

Source: Path instructions


1164-1164: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

삭제 안내 문구를 배정 삭제 기준으로 수정해 주세요.

이 액션은 후보가 아니라 스케줄 배정을 삭제합니다. 현재 문구는 가능 후보 목록에서도 제거되는 것처럼 오해될 수 있습니다.

수정 예시
-              이 스케줄을 삭제하면 해당 근무 후보가 목록에서 제거됩니다.
+              이 스케줄을 삭제하면 해당 추천 배정이 목록에서 제거됩니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/features/schedules/components/mvp-schedules-page.tsx` at line
1164, The delete confirmation copy in mvp-schedules-page should be changed
because it currently implies removing a candidate from the list, while this
action actually deletes a schedule assignment. Update the 안내 문구 in the deletion
flow around the schedule delete UI to clearly state that the selected schedule
assignment will be removed, not the candidate, and ensure the wording matches
the actual behavior of the delete action in the relevant component.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/modules/availability/availability.service.ts`:
- Around line 237-255: 프루닝 대상 조회가 트랜잭션 밖에서 계산되어 삭제/재생성 시점과 스냅샷이 어긋나는 문제입니다.
`availability.service.ts`의 `replaceAvailability`와 `client.$transaction` 흐름에서
`prunedItems` 계산도 같은 트랜잭션 안으로 옮기고, `deleteMany`와 `createMany`가 같은 읽기 시점의 데이터만
사용하도록 `AvailabilityDatabaseClient` 기반의 트랜잭션 콜백 내부에서 조회부터 재생성까지 처리하세요.

In `@apps/api/src/modules/schedules/schedules.mapper.ts`:
- Around line 53-60: The matching logic in the schedules mapper is too strict
because it only marks a candidate as recommended when its workDate, startsAt,
and endsAt exactly match an assignment; update the comparison in the
candidate-to-assignment check so merged assignment ranges in the schedules
mapper still match their underlying candidate windows. Use the existing mapping
logic around the candidate/assignment comparison to treat a candidate as
recommended when it falls within or is covered by a merged assignment interval,
while keeping candidates as the full possible set and assignments as the
recommended/confirmed list for schedules recommend and draft.

---

Outside diff comments:
In `@apps/web/src/features/schedules/components/mvp-schedules-page.tsx`:
- Around line 416-422: `canGenerate` in `mvp-schedules-page.tsx` only checks the
minimum staffing rules, so the recommendation action can still be enabled when
there are zero workers. Update the generation guard and any related
button/message logic to also require at least one worker, alongside the existing
checks in `canGenerate` and the schedule generation flow, so the UI matches the
server-side validation before invoking the recommendation request.
- Line 1164: The delete confirmation copy in mvp-schedules-page should be
changed because it currently implies removing a candidate from the list, while
this action actually deletes a schedule assignment. Update the 안내 문구 in the
deletion flow around the schedule delete UI to clearly state that the selected
schedule assignment will be removed, not the candidate, and ensure the wording
matches the actual behavior of the delete action in the relevant component.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 3a952fa7-eb78-4021-9e1f-490e190b3f73

📥 Commits

Reviewing files that changed from the base of the PR and between 9bd3ea3 and 4cd3a1a.

📒 Files selected for processing (11)
  • apps/api/src/modules/availability/availability.service.ts
  • apps/api/src/modules/schedules/schedules.mapper.ts
  • apps/api/src/modules/schedules/schedules.recommendation.ts
  • apps/api/src/modules/scheduling-time-policy.ts
  • apps/api/src/modules/staffing-rules/staffing-rules.service.ts
  • apps/web/next.config.ts
  • apps/web/package.json
  • apps/web/src/features/availability/components/mvp-availability-page.tsx
  • apps/web/src/features/schedules/components/mvp-schedules-page.tsx
  • apps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsx
  • docs/openapi.yaml

Comment on lines +237 to +255
const replaceAvailability = async (transaction: AvailabilityDatabaseClient) => {
await transaction.workerAvailableTime.deleteMany({
where: {
worker: {
organizationId,
},
availableDate: dateRange,
},
});

if (prunedItems.length > 0) {
await transaction.workerAvailableTime.createMany({
data: prunedItems,
});
}
};

if (isPrismaClient(client)) {
await client.$transaction(replaceAvailability);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

프루닝 대상 조회도 같은 트랜잭션 안으로 넣어 주세요.

replaceAvailability는 트랜잭션에서 삭제/재생성하지만, 재생성할 prunedItems는 트랜잭션 시작 전에 계산됩니다. 동시 저장이 끼어들면 새 가능 시간이 삭제되고 이전 스냅샷만 다시 생성될 수 있습니다.

수정 예시
-  const existingItems = await client.workerAvailableTime.findMany({
-    where: {
-      worker: {
-        organizationId,
-      },
-      availableDate: dateRange,
-    },
-    orderBy: [{ availableDate: "asc" }, { startsAt: "asc" }],
-  });
-  const prunedItems = createPrunedAvailabilityItems(organization, existingItems);
-
   const replaceAvailability = async (transaction: AvailabilityDatabaseClient) => {
+    const existingItems = await transaction.workerAvailableTime.findMany({
+      where: {
+        worker: {
+          organizationId,
+        },
+        availableDate: dateRange,
+      },
+      orderBy: [{ availableDate: "asc" }, { startsAt: "asc" }],
+    });
+    const prunedItems = createPrunedAvailabilityItems(organization, existingItems);
+
     await transaction.workerAvailableTime.deleteMany({
       where: {
         worker: {
📝 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
const replaceAvailability = async (transaction: AvailabilityDatabaseClient) => {
await transaction.workerAvailableTime.deleteMany({
where: {
worker: {
organizationId,
},
availableDate: dateRange,
},
});
if (prunedItems.length > 0) {
await transaction.workerAvailableTime.createMany({
data: prunedItems,
});
}
};
if (isPrismaClient(client)) {
await client.$transaction(replaceAvailability);
const replaceAvailability = async (transaction: AvailabilityDatabaseClient) => {
const existingItems = await transaction.workerAvailableTime.findMany({
where: {
worker: {
organizationId,
},
availableDate: dateRange,
},
orderBy: [{ availableDate: "asc" }, { startsAt: "asc" }],
});
const prunedItems = createPrunedAvailabilityItems(organization, existingItems);
await transaction.workerAvailableTime.deleteMany({
where: {
worker: {
organizationId,
},
availableDate: dateRange,
},
});
if (prunedItems.length > 0) {
await transaction.workerAvailableTime.createMany({
data: prunedItems,
});
}
};
if (isPrismaClient(client)) {
await client.$transaction(replaceAvailability);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/modules/availability/availability.service.ts` around lines 237 -
255, 프루닝 대상 조회가 트랜잭션 밖에서 계산되어 삭제/재생성 시점과 스냅샷이 어긋나는 문제입니다.
`availability.service.ts`의 `replaceAvailability`와 `client.$transaction` 흐름에서
`prunedItems` 계산도 같은 트랜잭션 안으로 옮기고, `deleteMany`와 `createMany`가 같은 읽기 시점의 데이터만
사용하도록 `AvailabilityDatabaseClient` 기반의 트랜잭션 콜백 내부에서 조회부터 재생성까지 처리하세요.

Comment on lines +53 to +60
assignment.workDate.getTime() === candidate.workDate.getTime() &&
assignment.startsAt.getTime() === candidate.startsAt.getTime() &&
assignment.endsAt.getTime() === candidate.endsAt.getTime() &&
(candidate.workerId !== null
? assignment.workerId === candidate.workerId
: assignment.workerId === null &&
assignment.workerNameSnapshot === candidate.workerNameSnapshot &&
assignment.employeeCodeSnapshot === candidate.employeeCodeSnapshot),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

병합된 배정 구간도 후보 추천 여부로 매칭하세요.

추천 엔진은 인접 배정을 병합해 반환하지만 후보는 수요 윈도우 단위로 남습니다. 현재 Line 53-55의 정확 일치 비교 때문에 10:00-12:00 배정 안의 10:00-11:00, 11:00-12:00 후보가 모두 isRecommended: false가 됩니다.

수정 제안
 const isCandidateRecommended = (
   candidate: PrismaScheduleCandidate,
   assignments: PrismaScheduleAssignment[],
 ) =>
   assignments.some(
-    (assignment) =>
-      assignment.workDate.getTime() === candidate.workDate.getTime() &&
-      assignment.startsAt.getTime() === candidate.startsAt.getTime() &&
-      assignment.endsAt.getTime() === candidate.endsAt.getTime() &&
-      (candidate.workerId !== null
-        ? assignment.workerId === candidate.workerId
-        : assignment.workerId === null &&
-          assignment.workerNameSnapshot === candidate.workerNameSnapshot &&
-          assignment.employeeCodeSnapshot === candidate.employeeCodeSnapshot),
+    (assignment) => {
+      const coversCandidate =
+        assignment.workDate.getTime() === candidate.workDate.getTime() &&
+        assignment.startsAt.getTime() <= candidate.startsAt.getTime() &&
+        assignment.endsAt.getTime() >= candidate.endsAt.getTime();
+
+      if (!coversCandidate) {
+        return false;
+      }
+
+      return candidate.workerId !== null
+        ? assignment.workerId === candidate.workerId
+        : assignment.workerId === null &&
+            assignment.workerNameSnapshot === candidate.workerNameSnapshot &&
+            assignment.employeeCodeSnapshot === candidate.employeeCodeSnapshot;
+    },
   );

As per path instructions, /schedules/recommend/schedules/draftcandidates를 가능 후보 전체로, assignments를 추천/확정 배정 목록으로 유지해야 합니다.

📝 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
assignment.workDate.getTime() === candidate.workDate.getTime() &&
assignment.startsAt.getTime() === candidate.startsAt.getTime() &&
assignment.endsAt.getTime() === candidate.endsAt.getTime() &&
(candidate.workerId !== null
? assignment.workerId === candidate.workerId
: assignment.workerId === null &&
assignment.workerNameSnapshot === candidate.workerNameSnapshot &&
assignment.employeeCodeSnapshot === candidate.employeeCodeSnapshot),
const coversCandidate =
assignment.workDate.getTime() === candidate.workDate.getTime() &&
assignment.startsAt.getTime() <= candidate.startsAt.getTime() &&
assignment.endsAt.getTime() >= candidate.endsAt.getTime();
if (!coversCandidate) {
return false;
}
return candidate.workerId !== null
? assignment.workerId === candidate.workerId
: assignment.workerId === null &&
assignment.workerNameSnapshot === candidate.workerNameSnapshot &&
assignment.employeeCodeSnapshot === candidate.employeeCodeSnapshot;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/modules/schedules/schedules.mapper.ts` around lines 53 - 60, The
matching logic in the schedules mapper is too strict because it only marks a
candidate as recommended when its workDate, startsAt, and endsAt exactly match
an assignment; update the comparison in the candidate-to-assignment check so
merged assignment ranges in the schedules mapper still match their underlying
candidate windows. Use the existing mapping logic around the
candidate/assignment comparison to treat a candidate as recommended when it
falls within or is covered by a merged assignment interval, while keeping
candidates as the full possible set and assignments as the recommended/confirmed
list for schedules recommend and draft.

Sources: Path instructions, Learnings

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/components/common/password-input.tsx (1)

22-26: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

토글 액션과 아이콘 방향을 맞추세요.

현재 숨김 상태에서는 aria-label="비밀번호 보기"인데 EyeOff가 보이고, 표시 상태에서는 “숨기기” 액션인데 Eye가 보입니다. 버튼이 수행할 동작 기준으로 아이콘을 다시 맞추는 편이 혼동이 적습니다.

수정 예시
-        {visible ? <Eye /> : <EyeOff />}
+        {visible ? <EyeOff /> : <Eye />}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/common/password-input.tsx` around lines 22 - 26, The
password toggle in password-input.tsx has the icon state reversed relative to
the action label. Update the conditional rendering around the toggle button so
the icon used by the visible state matches the action implied by the aria-label,
and the hidden state matches its own action; adjust the Eye/EyeOff swap in the
same component to keep the button behavior and visual cue aligned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsx`:
- Around line 579-588: The end-time Select in MVP staffing rules currently
allows options before the chosen start time, which lets overnight ranges escape
the organization’s operating hours. Update the logic around
createTimeOptions/createTimeOptionsFromRange and the relevant state in
mvp-staffing-rules-page.tsx so the end options are disabled until a start time
is chosen, or are generated only from the selected start time through the
operating-hour end boundary. Apply the same constraint consistently in the batch
flow and any related validation/schema paths so start/end time checks are
enforced by both the UI and the schema.

---

Outside diff comments:
In `@apps/web/src/components/common/password-input.tsx`:
- Around line 22-26: The password toggle in password-input.tsx has the icon
state reversed relative to the action label. Update the conditional rendering
around the toggle button so the icon used by the visible state matches the
action implied by the aria-label, and the hidden state matches its own action;
adjust the Eye/EyeOff swap in the same component to keep the button behavior and
visual cue aligned.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 5346ea2d-102f-412f-8899-7171aa8c26b8

📥 Commits

Reviewing files that changed from the base of the PR and between 4cd3a1a and 123b0ad.

📒 Files selected for processing (3)
  • apps/web/src/components/common/password-input.tsx
  • apps/web/src/features/schedules/components/mvp-schedules-page.tsx
  • apps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsx

Comment thread apps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsx Outdated
@ehlung
ehlung merged commit b31a3e8 into develop Jun 29, 2026
4 checks passed
@ehlung
ehlung deleted the feature/scheduling-policy-integration branch June 29, 2026 16:56
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.

2 participants