-
Notifications
You must be signed in to change notification settings - Fork 2
Open
Description
배경
AS-IS
- 팀 단위로 모든 팀원에게 동일한 문제 추천
- 추천 설정(난이도, 요일, 태그, 문제수)이 팀 레벨에 존재
문제점
- 팀원 간 실력 차이로 인해 난이도가 맞지 않는 경우 발생
- 팀원마다 다른 난이도의 문제를 받고 싶지만 불가능
TO-BE
- 팀 내 N개의 스쿼드를 구성하여 스쿼드별 추천 설정 가능
- 실력별로 스쿼드를 나눠 각자에게 맞는 난이도의 문제 추천
설계 결정 사항
| 항목 | 결정 | 비고 |
|---|---|---|
| 명칭 | Squad (스쿼드) | |
| 기본 스쿼드 | 팀 생성 시 자동 생성 | 삭제 불가 |
| 다중 소속 | 1인 1스쿼드 | squad_id NOT NULL (Contract 배포 시 제약 추가) |
| 미배정 허용 | 불허 | 팀 가입 시 기본 스쿼드 자동 배정 |
| 스쿼드 리더 | 없음 | 팀장이 모든 스쿼드 관리 |
| 멤버 관리 | TeamMember에 squadId 추가 | 별도 SquadMember 테이블 X |
| 추천 설정 | Squad 독립 설정 | Team 추천 설정 필드는 추후 별도 PR에서 제거 |
| Team 추천 API | deprecated 처리 | 응답에 deprecationMessage 추가, API 자체는 유지 |
| TeamIncludeTag | 일단 병렬 유지 | SquadIncludeTag 신규 생성, TeamIncludeTag는 추후 제거 |
ERD
AS-IS
erDiagram
Team ||--o{ TeamMember : has
Team ||--o{ TeamIncludeTag : has
Team ||--o{ Recommendation : generates
TeamMember }o--|| Member : is
Recommendation ||--o{ RecommendationProblem : contains
Recommendation ||--o{ MemberRecommendation : tracks
Team {
Long id PK
String name
String description
Boolean isPrivate
RecommendationStatus recommendationStatus
Integer recommendationDays
ProblemDifficultyPreset problemDifficultyPreset
Integer minProblemLevel
Integer maxProblemLevel
Integer problemCount
}
TO-BE
erDiagram
Team ||--o{ TeamMember : has
Team ||--o{ Squad : contains
Squad ||--o{ SquadIncludeTag : has
Squad ||--o{ Recommendation : generates
TeamMember }o--|| Member : is
TeamMember }o--|| Squad : belongs
Recommendation ||--o{ RecommendationProblem : contains
Recommendation ||--o{ MemberRecommendation : tracks
Team {
Long id PK
String name
String description
Boolean isPrivate
}
Squad {
Long id PK
Long teamId FK
String name
String description
Boolean isDefault
RecommendationStatus recommendationStatus
Integer recommendationDays
ProblemDifficultyPreset problemDifficultyPreset
Integer minProblemLevel
Integer maxProblemLevel
Integer problemCount
}
TeamMember {
Long id PK
Long teamId FK
Long memberId FK
Long squadId FK
TeamRole role
}
Recommendation {
Long id PK
Long teamId
Long squadId
RecommendationType type
}
MemberRecommendation {
Long id PK
Long memberId FK
Long recommendationId FK
Long teamId
String teamName
Long squadId
EmailSendStatus emailSendStatus
}
Recommendation/MemberRecommendation의 teamId, squadId: FK가 아닌 일반 Long — 팀/스쿼드 삭제 후에도 이력 보존
도메인 책임 분리
flowchart LR
subgraph Team도메인["team/"]
Team["Team\n팀원 관리"]
Squad["Squad\n추천 설정"]
TeamMember["TeamMember"]
end
subgraph Recommendation도메인["recommendation/"]
Rec["Recommendation\n추천 생성/발송"]
end
Squad --> Rec
TeamMember --> Squad
API 설계
v1 — Deprecated
Contract 배포 시 (프론트엔드 v2 전환 완료 이후) 완전 제거
| Method | URL | 대체 |
|---|---|---|
| GET | /api/teams/{teamId} |
GET /api/v2/teams/{teamId} |
| GET | /api/teams/public |
GET /api/v2/teams/public |
| GET | /api/teams/my |
GET /api/v2/teams/my |
| GET | /api/teams/{teamId}/recommendation-settings |
Squad 추천설정 API로 대체 |
| PUT | /api/teams/{teamId}/recommendation-settings |
Squad 추천설정 API로 대체 |
| DELETE | /api/teams/{teamId}/recommendation-settings |
Squad 추천설정 API로 대체 |
| POST | /api/recommendation/team/{teamId}/manual |
POST /api/teams/{teamId}/squads/{squadId}/recommendation/manual |
v2 — 팀 페이지 (스쿼드 기반)
| Method | URL | 설명 |
|---|---|---|
| GET | /api/v2/teams/{teamId} |
팀 페이지 (스쿼드 포함) |
| GET | /api/v2/teams/public |
공개 팀 목록 |
| GET | /api/v2/teams/my |
내 팀 목록 |
GET /api/v2/teams/{teamId} 응답 구조:
TeamPageResponse (v2)
├── TeamInfo (id, name, description, isPrivate, memberCount)
├── List<TeamMemberResponse> (memberId, handle, email, role, isMe, squadId, squadName)
├── List<SquadSummaryResponse>
│ └── squadId, name, description, isDefault, memberCount
│ isActive, recommendationDays, problemDifficultyPreset,
│ minProblemLevel, maxProblemLevel, problemCount, includeTags
└── TodayProblemResponse (로그인 유저의 스쿼드 기준)
Squad CRUD (팀 리더 전용)
| Method | URL | 설명 |
|---|---|---|
| POST | /api/teams/{teamId}/squads |
스쿼드 생성 (비활성 상태로 생성, 최대 5개) |
| GET | /api/teams/{teamId}/squads |
스쿼드 목록 조회 |
| PUT | /api/teams/{teamId}/squads/{squadId} |
스쿼드 이름/설명 수정 |
| DELETE | /api/teams/{teamId}/squads/{squadId} |
스쿼드 삭제 (기본 스쿼드 불가) |
| GET | /api/teams/{teamId}/squads/{squadId}/recommendation-settings |
추천 설정 조회 |
| PUT | /api/teams/{teamId}/squads/{squadId}/recommendation-settings |
추천 설정 수정 (태그 포함 전체 교체) |
| DELETE | /api/teams/{teamId}/squads/{squadId}/recommendation-settings |
추천 비활성화 |
| PUT | /api/teams/{teamId}/members/{memberId}/squad |
팀원 스쿼드 변경 |
| POST | /api/teams/{teamId}/squads/{squadId}/recommendation/manual |
수동 추천 생성 |
비즈니스 규칙
- 기본 스쿼드(isDefault=true)는 삭제 불가
- 스쿼드는 팀당 최대 5개 (기본 스쿼드 포함)
- 스쿼드 삭제 시 소속 멤버는 기본 스쿼드로 자동 이동
- 스쿼드 삭제 시 해당 스쿼드의 기존 Recommendation은 그대로 유지
- 팀 가입 승인 시 기본 스쿼드에 자동 배정
- 추천 활성화:
recommendationDays1개 이상 → ACTIVE, 빈 배열 → INACTIVE - 추천 중복 체크: 팀 기준 → 스쿼드 기준 (같은 팀 내 다른 스쿼드는 별도 추천 가능)
API 접근 권한
- 스쿼드 목록 조회: 팀원 전체 가능
- 스쿼드 생성/수정/삭제/추천설정 변경: 팀장 전용
- 팀원 스쿼드 변경: 팀장 전용
배포 전략 (Expand-Contract 패턴)
블루-그린 배포 환경에서 무중단으로 마이그레이션하기 위해 Expand-Contract 패턴을 적용한다.
배포 단계별 흐름
flowchart TD
A[1차 DB 배포\nExpand: 스키마 추가만] --> B[1차 스프링 배포\nDual Write + 구 스키마 기준 동작]
B --> C[2차 DB 배포\nBackfill: 기존 데이터 보정]
C --> D[2차 스프링 배포\nDual Write 제거 + 신규 스키마 기준 동작]
D --> E[Contract 배포 - 추후 별도 PR\n구 스키마 제거]
1차 DB 배포 — Expand (스키마 추가만)
기존 스프링 서버가 운영 중인 상태에서 실행. 모든 변경이 nullable이므로 기존 코드에 영향 없음.
마이그레이션 파일: V2__add_squad_schema.sql
-- Squad 테이블 생성
CREATE TABLE squad ( ... );
-- SquadIncludeTag 테이블 생성
CREATE TABLE squad_include_tag ( ... );
-- TeamMember에 squad_id 추가 (nullable)
ALTER TABLE team_member ADD COLUMN squad_id BIGINT NULL;
-- Recommendation에 squad_id 추가 (nullable)
ALTER TABLE recommendation
ADD COLUMN squad_id BIGINT NULL;
-- MemberRecommendation에 squad_id 추가 (nullable)
ALTER TABLE member_recommendation
ADD COLUMN squad_id BIGINT NULL;
⚠️ squad_id NOT NULL 제약은 이 단계에서 추가하지 않는다. 기존 서버가 squad_id를 모르는 상태에서 NOT NULL을 걸면 TeamMember INSERT 시 에러 발생.
1차 스프링 배포 — Dual Write (구 스키마 기준 동작)
새 스키마에 동시에 쓰되, 읽기/동작은 여전히 Team 기준 유지.
코드 변경:
- Squad 도메인 전체 구현 (Entity, Repository, Service, Controller)
- Squad CRUD API + 팀원 스쿼드 변경 API
- 팀 생성 시 → Team 저장 + 기본 Squad 동시 생성
- 추천 설정 변경 시 → Team + Squad 동시 업데이트 (Dual Write)
- 팀 가입 승인 시 → TeamMember 저장 + squad_id 기본 스쿼드로 설정
- 스쿼드 삭제 시 → 소속 멤버 기본 스쿼드로 이동
- 스케줄러/읽기: Team 기준 유지 (동작 변경 없음)
블루-그린 전환 안전성:
- 구 Blue 서버: Squad 테이블/컬럼 미사용 → nullable이므로 INSERT 정상
- 신 Green 서버: Dual Write로 두 스키마 동시 유지
- 전환 구간에 팀 추천설정이 변경되어도 Green이 Squad까지 동기화
2차 DB 배포 — Backfill (기존 데이터 보정)
1차 스프링 배포 이전에 생성된 데이터(Dual Write 이전) 보정. 1차 스프링이 운영 중인 상태에서 실행.
마이그레이션 파일: V3__backfill_squad_data.sql
-- 기존 팀마다 기본 Squad 생성 (Dual Write로 이미 생성된 팀은 스킵)
INSERT INTO squad (team_id, name, is_default, recommendation_status, ...)
SELECT id, '기본 스쿼드', true, recommendation_status, ...
FROM team
WHERE NOT EXISTS (
SELECT 1 FROM squad WHERE squad.team_id = team.id AND is_default = true
);
-- TeamIncludeTag → SquadIncludeTag 복사 (미복사분만)
INSERT INTO squad_include_tag (squad_id, tag)
SELECT s.id, t.tag
FROM team_include_tag t
JOIN squad s ON s.team_id = t.team_id AND s.is_default = true
WHERE NOT EXISTS (
SELECT 1 FROM squad_include_tag sit
WHERE sit.squad_id = s.id AND sit.tag = t.tag
);
-- TeamMember.squad_id 보정 (squad_id IS NULL인 것만)
UPDATE team_member tm
JOIN squad s ON s.team_id = tm.team_id AND s.is_default = true
SET tm.squad_id = s.id
WHERE tm.squad_id IS NULL;WHERE ... IS NULL / NOT EXISTS: Dual Write로 이미 채워진 데이터는 덮어쓰지 않음. 1차 스프링이 운영 중에 실행되어도 안전.
2차 스프링 배포 — Contract 1단계 (신규 스키마 기준 전환)
Dual Write 제거, Squad 기준으로 전환.
코드 변경:
- Dual Write 코드 제거 (Squad만 업데이트)
- 스케줄러: Team → Squad 기준으로 전환
- 추천 중복 체크: 팀 기준 → 스쿼드 기준
- 수동 추천 API: SquadController로 이동
- Team 추천 API:
deprecationMessage필드 추가
배포 시간대 제한:
- 스케줄러 실행 시간(새벽 6시, 오전 9시) 전후 배포 금지
- 블루-그린 전환 구간에 스케줄러 중복 실행 방지
Contract 배포 — 추후 별도 PR
| 작업 | 내용 |
|---|---|
| squad_id NOT NULL 제약 추가 | 이 시점엔 모든 서버가 squad_id를 씀 |
| Team 추천 설정 컬럼 제거 | |
| TeamIncludeTag 테이블 제거 | |
| Team 추천 API 완전 제거 |
추후 개선 사항
스쿼드 중간 합류 정책
현재 (1차 구현):
- 스쿼드 변경 즉시 반영
- 중간 합류자도 기존 로직대로 미션 참여 및 해결 인증 가능
문제점:
- 합류 전에 이미 푼 문제가 미션에 있을 경우 공정성 이슈
- solved.ac에서는 풀이 시점을 알 수 없어 합류 후 풀었는지 검증 불가
개선 방향 (추후):
스쿼드 변경 시:
1. TeamMember.squadId 즉시 업데이트
2. 해당 미션 사이클: 조회 불가 + 해결 인증 불가
3. 다음 사이클(06:00)부터 새 스쿼드 미션 참여
구현 방식:
- MemberRecommendation에 squadId 스냅샷 저장 (현재 teamId처럼)
- 미션 조회/인증 시:
MemberRecommendation.squadId == 현재 squadId체크 - 불일치 시 "다음 미션부터 참여 가능" 안내
결정 완료 항목
- 스쿼드 설정 항목 → Squad 독립 설정 (요일, 난이도, 문제 수, 태그)
- 기본 스쿼드 삭제 → 불가
- 스쿼드 최대 개수 제한 → 팀당 최대 5개 (기본 스쿼드 포함)
- 스쿼드 없는 팀원 허용 → 불허 (squad_id NOT NULL, Contract 배포 시 제약 추가)
- 명칭 → Squad 확정
- Team 추천 설정 API → deprecated 필드 추가로 유지, 실제 제거는 추후 PR
- 수동 추천 API →
POST /api/teams/{teamId}/squads/{squadId}/recommendation/manual으로 변경 - 이메일 → 팀명만 유지 (squad명 추가 없음)
- 신규 스쿼드 초기 상태 → 비활성 (추천 설정 후 직접 활성화)
- Contract 배포 시점 → 프론트엔드 deprecated API 사용 완전 제거 이후
- 배포 전략 → Expand-Contract 패턴 (1차 DB → 1차 스프링 → 2차 DB Backfill → 2차 스프링)
- 마이그레이션 SQL → 배포당 파일 하나 (
V2__add_squad_schema.sql,V3__backfill_squad_data.sql) - API 접근 권한 → 스쿼드 목록 조회는 팀원 가능, 나머지 변경 작업은 팀장 전용
- 스쿼드 삭제 시 기존 Recommendation → 그대로 유지
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Type
Projects
Status
In progress