feat: 스케줄 추천 정책 통합 - #98
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 46 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Walkthrough최소 인원 조건을 영업시간과 교집합해 스케줄링 가능 구간을 계산하는 공용 정책 모듈을 추가하고, 가용 시간 저장·조회와 최소 인원 조건 변경 시 해당 구간 밖 데이터를 프루닝하도록 연결했습니다. 스케줄 추천은 수요 구간 기반으로 재구성되어 후보와 배정이 함께 저장·반환되며, 웹 화면과 문서 계약도 이에 맞게 갱신됐습니다. Changes최소 인원 조건 기반 스케줄링 정책
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (31)
apps/api/src/modules/availability/availability.service.spec.tsapps/api/src/modules/availability/availability.service.tsapps/api/src/modules/schedules/schedules.mapper.tsapps/api/src/modules/schedules/schedules.recommendation.tsapps/api/src/modules/schedules/schedules.service.spec.tsapps/api/src/modules/schedules/schedules.service.tsapps/api/src/modules/scheduling-time-policy.tsapps/api/src/modules/staffing-rules/staffing-rules.service.spec.tsapps/api/src/modules/staffing-rules/staffing-rules.service.tsapps/api/test/availability.routes.test.tsapps/api/test/helpers/fake-prisma.tsapps/api/test/schedules.routes.test.tsapps/api/test/staffing-rules.routes.test.tsapps/web/src/components/ui/select.tsxapps/web/src/features/availability/components/mvp-availability-page.tsxapps/web/src/features/schedules/api/schedules-api.tsapps/web/src/features/schedules/components/mvp-schedules-page.tsxapps/web/src/features/schedules/queries/schedules-queries.tsapps/web/src/features/schedules/queries/schedules-query-keys.tsapps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsxapps/web/src/features/staffing-rules/queries/staffing-rules-queries.tsdocs/API.mddocs/ERD.mddocs/PRD.mddocs/SCHEDULING_POLICY.mddocs/SPEC.mddocs/openapi.yamlpackages/database/prisma/migrations/20260629000000_add_schedule_candidates/migration.sqlpackages/database/prisma/schema.prismapackages/shared/src/schemas/schedules.tspackages/shared/src/types/api/index.ts
| 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, | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
프루닝 후 가능 시간이 0건이면 추천 생성을 막아야 합니다.
Line 158에서 다시 조회한 availableTimes가 비어 있어도 그대로 inputHash와 createRecommendations로 넘어가서, 선택 기간에 가능 시간이 전혀 없는 경우에도 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.
| 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
| 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); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
익일 영업일의 자정 이후 조건이 스케줄 가능 구간에서 누락됩니다.
예: 금요일 영업시간 22:00~02:00(closesNextDay)에서 금요일 기준 00:00~01:00 최소 인원 조건은 0~60분으로 계산되어 1320~1560 영업 구간과 교집합이 없어집니다. closesNextDay 영업일에서 start/end가 openTime보다 작은 규칙은 +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
| const scheduleTargetDates = useMemo( | ||
| () => | ||
| new Set([ | ||
| ...schedules.map((item) => item.date), | ||
| ...candidates.map((item) => item.date), | ||
| ...unfilledConditions.map((condition) => condition.date), | ||
| ]), | ||
| [candidates, schedules, unfilledConditions], |
There was a problem hiding this comment.
🎯 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
| 정책 세부 기준은 `docs/SCHEDULING_POLICY.md`를 따릅니다. | ||
|
|
||
| - 조직이 있어야 합니다. | ||
| - 인력이 1명 이상 있어야 합니다. | ||
| - 선택 기간에 저장된 가능 시간이 1개 이상 있어야 합니다. | ||
| - 조직 영업시간이 설정되어 있어야 합니다. | ||
| - 최소 인원 조건이 1개 이상 있어야 합니다. | ||
| - 선택 기간에 저장된 가능 시간이 없으면 최소 인원 조건이 있는 전체 구간을 미충족 조건으로 기록할 수 있습니다. | ||
| - 같은 입력 조건으로 이미 생성된 DRAFT가 있으면 중복 생성하지 않습니다. | ||
|
|
||
| #### 추천 생성 동작 | ||
|
|
||
| 1. 사용자가 추천 생성을 요청합니다. | ||
| 2. 서버는 인력, 가능 시간, 최소 인원 조건, 선택 기간을 기준으로 DRAFT 스케줄을 생성합니다. | ||
| 3. 생성된 스케줄은 `DRAFT` 상태로 저장합니다. | ||
| 4. 최소 인원 조건을 충족하지 못한 날짜와 시간대가 있으면 미충족 조건으로 기록합니다. | ||
| 2. 서버는 조직 영업시간, 인력, 가능 시간, 최소 인원 조건, 선택 기간을 기준으로 DRAFT 스케줄을 생성합니다. | ||
| 3. 최소 인원 조건이 있는 시간대에서만 가능한 후보를 산출합니다. | ||
| 4. 가능 후보 중 추천 배정을 산출합니다. | ||
| 5. 추천 배정은 최소 인원 조건을 최대한 충족한 뒤 주간 계약 시간 대비 배정 균형을 기준으로 선택합니다. | ||
| 6. 모든 가능 후보의 계약 시간 사용률이 100% 이상이면 초과 폭이 가장 작은 근무자를 우선 선택하고 계약 시간 초과 경고 대상으로 표시합니다. | ||
| 7. 생성된 스케줄은 `DRAFT` 상태로 저장합니다. | ||
| 8. 최소 필요 인원 대비 추천 배정이 부족한 날짜와 시간대가 있으면 미충족 조건으로 기록합니다. |
There was a problem hiding this comment.
🗄️ 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.
| 정책 세부 기준은 `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.
There was a problem hiding this comment.
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
📒 Files selected for processing (11)
apps/api/src/modules/availability/availability.service.tsapps/api/src/modules/schedules/schedules.mapper.tsapps/api/src/modules/schedules/schedules.recommendation.tsapps/api/src/modules/scheduling-time-policy.tsapps/api/src/modules/staffing-rules/staffing-rules.service.tsapps/web/next.config.tsapps/web/package.jsonapps/web/src/features/availability/components/mvp-availability-page.tsxapps/web/src/features/schedules/components/mvp-schedules-page.tsxapps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsxdocs/openapi.yaml
| 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); |
There was a problem hiding this comment.
🗄️ 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.
| 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` 기반의 트랜잭션 콜백 내부에서 조회부터 재생성까지 처리하세요.
| 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), |
There was a problem hiding this comment.
🎯 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/draft는 candidates를 가능 후보 전체로, 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.
| 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
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
apps/web/src/components/common/password-input.tsxapps/web/src/features/schedules/components/mvp-schedules-page.tsxapps/web/src/features/staffing-rules/components/mvp-staffing-rules-page.tsx
요약
검증
Summary by CodeRabbit
candidates)과 추천 여부(isRecommended)가 추가되었습니다.