Skip to content

[Feat/#189] 빌릴게 근무 시간표 관리자 API 추가 - #193

Merged
xeoxxn merged 13 commits into
mainfrom
feat/#189-billilge-work-schedule-admin-api
Nov 4, 2026
Merged

xeoxxn merged 13 commits into
mainfrom
feat/#189-billilge-work-schedule-admin-api

Conversation

@xeoxxn

@xeoxxn xeoxxn commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

#️⃣연관된 이슈

🎯 해결하려는 문제가 무엇인가요?

관리자 기타 관리 → 근무 시간표 관리 화면이 쓸 API가 없었습니다. 화면은 월금, 1017시 그리드에 근무자를 1시간 단위로 배치하고 주 단위로 넘겨 보는데, 지금 work_schedules는 요일(day_of_week)만 저장해서 주마다 다른 시간표를 담을 수 없고, 이 테이블에 쓰는 코드도 없었습니다.

명세의 5개 엔드포인트를 만들었습니다.

엔드포인트 화면
GET /v1/admin/billilge/work-schedules?date= 주 단위 시간표 그리드 (date 생략 시 이번 주)
GET /v1/admin/billilge/work-schedules/workers 등록·수정 모달의 근무자 드롭다운
POST /v1/admin/billilge/work-schedules + 근무 시간 등록 — 근무자 1명 + 시간대 여러 개
PUT /v1/admin/billilge/work-schedules/{scheduleId} 근무 시간 수정 모달 — 근무자·근무일·시간 전체 교체
DELETE /v1/admin/billilge/work-schedules/{scheduleId} 근무 시간 삭제

❓ 왜 해결해야 하나요?

빌릴게 근무자 배치를 관리자 화면에서 할 수 있어야 하는데, 화면은 만들어져 있어도 붙일 API가 없어 mock 데이터로만 돌고 있습니다. 요일만 저장하는 지금 구조로는 "다음 주 월요일만 바꾸기"도, "지난 주 기록은 그대로 두기"도 할 수 없습니다.

⭐ 어떻게 해결했나요?

1. 요일 대신 근무일을 저장합니다 (V48·V49)

schedule_id | member_id | work_date  | start_hour | end_hour
1           | 7         | 2026-10-26 | 10         | 11        ← 이번 주 월
2           | 7         | 2026-11-02 | 10         | 11        ← 다음 주 월 (별개 행)

요일은 저장하지 않고 응답을 만들 때 workDate.getDayOfWeek()로 계산합니다. 기존 행은 어느 주의 근무인지 알 수 없어 날짜로 옮길 수 없고, 이 테이블에 쓰는 API도 없었기 때문에 비우고(V48) 컬럼을 바꿨습니다(V49). 데이터 삭제와 스키마 변경은 flyway-migration.md 3-5절대로 파일을 나눴습니다.

2. 근무 시간 규칙은 도메인이, 오늘·다른 근무 시간이 필요한 규칙은 서비스가 검증합니다

규칙 위치 에러
월~금만 WorkSchedule.create/change 400 INVALID_WORK_DATE
1017시, 시작 < 종료, 점심시간(1213시)에 걸치지 않음 WorkSchedule.create/change 400 INVALID_WORK_HOURS
지난 주가 아님 (수정은 옮기기 전·후 둘 다) AdminWorkScheduleServiceImpl.validateNotPastWeek 400 PAST_WEEK_NOT_EDITABLE
한 시간 칸에 한 명 (근무자가 달라도, 요청 안끼리도) AdminWorkScheduleServiceImpl.validateNotConflicted 409 WORK_SCHEDULE_CONFLICT

FeedbackRound.create의 기간 검증처럼 값 하나로 판단할 수 있는 규칙은 생성할 때 막고, 지난 주·겹침처럼 오늘 날짜나 다른 행이 필요한 규칙은 서비스의 validate~에 뒀습니다. 지난 주는 날짜가 아니라 주(월요일) 단위로 판단해서 이번 주의 지난 요일은 고칠 수 있습니다.

3. "근무자"는 근무 시간표 쪽에서 조립합니다

근무자는 **학생회 부서가 있는 운영진(role ADMIN)**입니다. 운영진에서 내려가 부서 값만 남은 회원은 빠집니다(AuthTokenIssuer가 ADMIN에게만 부서를 싣는 것과 같은 기준). 이 판단을 member에 두면 member가 빌릴게 근무라는 개념에 묶이므로, member에는 사실만 범용으로 두고 해석은 AdminWorkScheduleUseCase가 합니다.

  • member: Member.isCouncilMember()(ADMIN이면서 학생회 부서가 있음), MemberService.findAllCouncilMembers() 추가 (기존 메서드는 그대로)
  • AdminWorkScheduleUseCase: 근무자 목록 = 학생회 구성원, 배정할 회원 확인 = getById(없거나 탈퇴면 MEMBER_NOT_FOUND) 후 학생회 구성원이 아니면 WORKER_NOT_FOUND

근무자 규칙이 바뀌면(예: 회장단 제외) 이 UseCase만 고치면 됩니다. 근무 시간(internal)에 이름·학번(member)을 붙이는 조합도 여기서 합니다(AdminFeedbackSearchUseCase와 같은 방식).

🧩 이 PR의 한계 & 트레이드오프

  • 동시 등록 구멍: 겹침은 "같은 값"이 아니라 "구간이 맞물림"이라 유니크 제약으로 막을 수 없어서, 저장 전에 조회해서 확인합니다. 두 관리자가 같은 칸을 거의 동시에 저장하면 둘 다 통과할 수 있습니다. 운영진 몇 명이 쓰는 화면이라 드물고, 생겨도 시간표에 보여서 하나를 지우면 되므로 감수했습니다(피드백 회차 기간 겹침과 같은 방식, 주석으로 남김).
  • 기존 work_schedules 행 삭제: 요일만 있는 데이터는 날짜로 옮길 방법이 없습니다.
  • date를 문자열로 받음: LocalDate로 받으면 형식 오류 메시지에 스프링 변환 오류 원문이 나가서, 이 PR에서는 WorkScheduleWeekParams가 문자열로 받아 toDate()에서 파싱합니다. 공통 GlobalExceptionHandler(기존 ArchiveListParams.year도 같은 상황)는 후속으로 남깁니다.

⛓️ 기존 기능에 미치는 영향

  • work_schedules 스키마가 바뀝니다. 이 테이블을 쓰던 코드는 없었습니다.
  • member 도메인은 메서드 추가만 있고 기존 동작은 바뀌지 않습니다.
  • Flyway V48·V49: main(V46)과 열린 PR이 쓰는 번호를 피해 다시 매겼습니다. 다른 브랜치가 먼저 머지되면 머지 직전에 다시 확인합니다.
  • 배포: V48 적용 전 SELECT COUNT(*) FROM work_schedules로 행이 있는지 확인하고, V49 적용 후에는 이전 버전 이미지로 되돌릴 수 없습니다(ddl-auto: validate에서 기동 실패). 두 파일 머리 주석에도 남겼습니다.

🔀 Edge Case & 실패 시나리오

상황 처리
date가 토·일 그 앞 월요일의 주를 돌려준다
date 형식 오류 (abc, 2026-13-01, 2026-02-30) 400 INVALID_INPUT "날짜는 yyyy-MM-dd 형식이어야 합니다."
탈퇴한 회원의 근무 시간 조회에 그대로 나오고 name·studentId는 null
학생회 구성원에서 빠진 회원(부서 해제·운영진 해제)의 근무 시간 조회에 그대로 나온다. 수정할 때 근무자를 그대로 두면 WORKER_NOT_FOUND라 다른 근무자로 바꿔야 한다
맞닿은 시간대 (1011시, 1112시) 겹침이 아니다. 합치지 않고 보낸 그대로 저장한다
수정 대상이 자기 원래 칸과 겹침 같은 id는 겹침 검사에서 뺀다
지난 주 근무를 이번 주로 / 이번 주 근무를 지난 주로 옮김 둘 다 400 PAST_WEEK_NOT_EDITABLE
등록 요청 중 하나만 규칙 위반 아무것도 저장하지 않는다 (한 트랜잭션)

📋 검토한 대안과 선택 이유

  • 1시간 칸마다 한 행 + (work_date, hour) 유니크: 동시 등록까지 DB가 막아 주지만, 화면이 구간 단위로 등록·수정·삭제하고 명세도 구간(startHour~endHour)이라 행 쪼개기·합치기 비용이 큽니다. 구간 저장 + 조회 후 확인을 택했습니다.
  • 저장 전 잠금(SELECT ... FOR UPDATE, named lock): 위 동시성 구멍을 막을 수 있지만 사용 빈도에 비해 코드가 늘어 보류했습니다.
  • 근무자 판정을 member에 (isWorker(), AdminMemberService): 처음엔 그렇게 만들었다가, member가 근무 개념에 묶이는 문제로 위 3번 구조로 바꿨습니다.
  • @RequestParam LocalDate date: GlobalExceptionHandler에 MethodArgumentTypeMismatchException 처리가 없어 형식 오류가 500이 됩니다. @ModelAttribute Params record로 받고, 그 안에서도 LocalDate가 아니라 문자열로 받아 직접 파싱합니다(EventApplicationListParams.cursor와 같은 방식).

💬 리뷰 포인트

  • AdminWorkScheduleServiceImpl.validateNotConflicted — 수정 대상 제외, 요청 안 겹침 판정이 맞는지
  • AdminWorkScheduleUseCase — 근무자 판정(학생회 부서가 있는 운영진)을 UseCase에 둔 구조
  • V48·V49 마이그레이션과 번호

명세(노션 「API 명세서」 빌릴게 근무 시간표 5건)는 관리자 화면과 대조해 근무자 기준·에러 코드를 고쳐 두었습니다(🔁 표시). ./gradlew check(ModularityTests·DomainImplAccessTests 포함) 통과했습니다.

@xeoxxn xeoxxn self-assigned this Oct 29, 2026
@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@@ -0,0 +1,3 @@
-- 요일만 저장된 근무 시간은 어느 주의 근무인지 알 수 없어 근무일로 옮길 수 없다.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[r] 리뷰 포인트 주신 V28·V29 마이그레이션과 번호에 대한 의견이에요. main에 이미 V28__replace_locker_section_layout_photo_with_list.sql과 V29__add_is_canceled_to_locker_applications.sql이 들어와 있어서, 이대로 머지하면 같은 버전이 두 개씩 생겨 Flyway가 기동할 때 실패해요. 파일명이 달라 git에서는 충돌로 잡히지 않는 부분이라(지금 GitHub에도 충돌 없음으로 나와요) 미리 말씀드려요. 본문의 "main은 V19까지"가 그사이 V29까지 올라갔어요.

outOfOrder 설정이 없어서(기본 false) 번호가 실제 머지 순서대로 올라가야 해요. 브랜치가 #171 머지 시점에서 갈라진 뒤로 PR 7개가 main에 들어왔으니, main을 받으신 뒤 머지 직전에 main과 열린 PR들이 쓰는 번호를 확인해서 다시 매겨주시면 좋겠습니다(flyway-migration.md 2절). 지금 열린 PR들도 V29~V34를 나눠 쓰고 있어요. 번호를 바꾸실 때 2번째 줄 주석의 (V29)도 같이 고쳐 주세요.

파일 구성은 좋아 보여요. 데이터 삭제와 스키마 변경을 3-5절대로 나누셨고, 기존 행은 날짜로 옮길 수 없으니 먼저 비운 뒤 work_date를 NOT NULL로 추가하는 순서도 맞아요. 인덱스 이름(idx_work_schedules_work_date)과 WorkScheduleJpaEntity의 @Index도 같이 맞춰 주셨고요.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

머지 직전에 main과 열린 PR 번호를 확인해서 다시 매기고, 주석의 (V29)도 같이 고치겠습니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

b172273 수정했습니다. main과 열린 PR 번호를 피해 V38·V39로 다시 매기고, 주석의 (V29)는 "바로 다음 마이그레이션"으로 바꿨습니다.

@@ -0,0 +1,7 @@
-- 근무 시간표는 주마다 달라서 요일 대신 근무일을 저장한다.
ALTER TABLE work_schedules
DROP COLUMN day_of_week,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[a] 컬럼을 지우는 마이그레이션이라 한 가지만 말씀드려요. 이 파일이 적용된 뒤 이전 버전 이미지로 롤백하면, 그 이미지의 WorkScheduleJpaEntity가 day_of_week를 찾지 못해 ddl-auto: validate에서 기동이 안 돼요. 테이블을 쓰는 코드가 없어 데이터 쪽은 괜찮지만 롤백이 막히는 건 그와 별개라, main의 V24·V25처럼 주석을 두 줄 남겨두면 배포하는 사람이 확인하기 좋을 것 같아요.

  • V28: 적용 전 SELECT COUNT(*) FROM work_schedules로 행이 있는지 확인한다
  • V29: 적용 후에는 이전 버전 이미지로 되돌릴 수 없다(ddl-auto: validate에서 기동 실패)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

V28에 적용 전 행 수 확인, V29에 롤백 불가 주석을 추가했습니다. b87209e

// 한 시간 칸에는 근무자 한 명만 들어간다. 저장된 근무 시간 위에 요청한 근무 시간을 하나씩 올려 보며
// 먼저 올라간 것(저장된 것·앞선 요청)과 겹치면 막는다. 근무자가 달라도 해당한다.
// 구간 겹침은 유니크 제약으로 막을 수 없어 저장 전에 확인한다(조회-후-삽입이라 동시 등록 구멍은 남는다)
private void validateNotConflicted(List<WorkSchedule> targets) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[a] 리뷰 포인트 주신 validateNotConflicted의 수정 대상 제외와 요청 안 겹침 판정은 맞아 보여요. 저장된 행이 id 1 = 화 1012시, id 2 = 화 1315시라고 두고 따라가 봤어요.

  • id 1을 화 10~11시로 수정 → findSavedSchedulesOnSameDates가 id 1을 빼서 id 2와만 비교하고 통과
  • id 1을 화 14~16시로 수정 → id 2와 겹쳐 409
  • 화 1516시와 화 1517시를 한 번에 등록 → 첫 건은 id 2와 맞닿기만 해서 placed에 올라가고, 둘째 건이 첫 건과 겹쳐 409. saveAll 전에 던지니 아무것도 저장되지 않아요

등록은 id가 null이라 빠지는 행이 없고 수정은 자기 id만 빠져서, 두 경로가 한 메서드로 깔끔하게 모였네요. 동시 등록 구멍을 주석과 PR 한계에 적어 두신 것도 AdminFeedbackServiceImpl.java:133의 회차 기간 겹침과 같은 방식이라 좋아 보여요.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

확인 감사합니다!

}

// 없는 회원(탈퇴 포함)은 MEMBER_NOT_FOUND, 학생회 부서가 없는 회원은 WORKER_NOT_FOUND다
private Member getWorker(Long memberId) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[a] 리뷰 포인트 주신 근무자 판정을 UseCase에 둔 구조는 좋아 보여요.

  • core:domain:internal은 core:common에만 의존해서 member를 읽을 수 없으니, 판정 자리는 UseCase가 맞아요(coding-style.md 2-7절: 다른 도메인 조합은 서비스가 아니라 api:admin-api의 UseCase가 맡는다).
  • 다른 도메인을 조회해 확인하고 자기 도메인 에러 코드를 던지는 모양이 FeeSubmitUseCase.validateProofImage(file을 조회해 FeeErrorCode.INVALID_PROOF_IMAGE)와 같고, 쓰기가 서비스 하나라 트랜잭션을 걸지 않는 이유를 클래스 주석에 남긴 것도 같아요.
  • member에는 hasCouncilDepartment()라는 사실만 둬서 member가 근무 개념을 몰라도 되는 점도 좋습니다.

하나만 확인하고 싶어요. AuthTokenIssuer.java:32는 role이 ADMIN이 아니면 학생회 부서를 없는 것으로 보고, CouncilDepartment 주석에도 "ADMIN에게만 부여"라고 되어 있어요. 근무자 판정은 부서만 보니, 운영진에서 내려가 role은 STUDENT인데 부서 값이 남아 있는 회원이 있으면 드롭다운에 계속 나와요. 운영진을 해제할 때 부서도 같이 비운다는 전제라면 지금 그대로 괜찮고요, 의도하신 동작인지 궁금합니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

role이 ADMIN이면서 학생회 부서가 있는 회원만 근무자로 보도록 수정했습니다. 6b699ca


@Schema(description = "조회할 주에 속한 날짜. 토·일이면 그 앞 월요일의 주, 생략하면 이번 주", example = "2026-10-21")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
LocalDate date

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[a] PR 한계에 적어주신 date 형식 오류 메시지는 후속으로 두셔도 될 것 같아요. GlobalExceptionHandler.handleValidation이 첫 FieldError의 기본 메시지를 그대로 내보내는 구조라 ArchiveListParams.year도 같은 상황이고, 공통 핸들러에서 한 번에 고치는 게 맞아 보여요. @RequestParam으로 받았다면 MethodArgumentTypeMismatchException을 받는 핸들러가 없어 Exception 핸들러에서 500이 됐을 테니, Params record로 받으신 선택도 좋습니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

date를 문자열로 받아 직접 파싱해서 형식 오류 메시지를 정리했습니다(공통 핸들러는 후속). 3156b15

…rk-schedule-admin-api

# Conflicts:
#	core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/member/domain/Member.java
@xeoxxn
xeoxxn merged commit ce6fa1b into main Nov 4, 2026
1 check passed
@xeoxxn
xeoxxn deleted the feat/#189-billilge-work-schedule-admin-api branch November 4, 2026 17:01
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.

빌릴게 근무 시간표 관리자 API 추가 (주 단위 조회·등록·수정·삭제·근무자 목록)

3 participants