-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
Description
[Refactoring] Tag 시스템 리팩토링
📋 이슈 개요
Institution과 Review가 각각 다른 방식으로 관리하는 태그를 Tag Entity로 통합하고, Member의 선호 태그를 추가하여 AI 추천 시스템에 활용합니다.
🎯 목적
- 구조 통일: Institution, Member, Review 모두 Tag Entity 사용
- Enum 제거: 중복된 Tag Enum들 삭제
- 동적 관리: 관리자가 웹에서 태그 추가/수정
- AI 추천: Member 선호 태그로 개인화 추천
📋 전체 API 엔드포인트 목록
Part 1: Tag 조회 (공개)
| Method | Endpoint | 설명 | 권한 | 상태 |
|---|---|---|---|---|
| GET | /api/v1/tags |
전체 태그 목록 | 공개 | ❌ 구현 필요 |
| GET | /api/v1/tags/category/{category} |
카테고리별 태그 목록 | 공개 | ❌ 구현 필요 |
| GET | /api/v1/tags/{tagId} |
태그 상세 조회 | 공개 | ❌ 구현 필요 |
Part 2: Member 선호 태그
| Method | Endpoint | 설명 | 권한 | 상태 |
|---|---|---|---|---|
| GET | /api/v1/members/me/preference-tags |
내 선호 태그 조회 | USER | ❌ 구현 필요 |
| PUT | /api/v1/members/me/preference-tags |
선호 태그 일괄 업데이트 | USER | ❌ 구현 필요 |
| POST | /api/v1/members/me/preference-tags/{tagId} |
선호 태그 추가 | USER | ❌ 구현 필요 |
| DELETE | /api/v1/members/me/preference-tags/{tagId} |
선호 태그 제거 | USER | ❌ 구현 필요 |
Part 3: Institution 태그 연결
| Method | Endpoint | 설명 | 권한 | 상태 |
|---|---|---|---|---|
| PUT | /api/v1/institutions/{institutionId}/tags |
기관 태그 업데이트 | OWNER, MANAGER | ❌ 구현 필요 |
Part 4: Tag 관리 (관리자)
| Method | Endpoint | 설명 | 권한 | 상태 |
|---|---|---|---|---|
| GET | /api/v1/admin/tags |
전체 태그 관리 목록 | ADMIN | ❌ 구현 필요 |
| POST | /api/v1/admin/tags |
태그 생성 | ADMIN | ❌ 구현 필요 |
| PUT | /api/v1/admin/tags/{tagId} |
태그 수정 | ADMIN | ❌ 구현 필요 |
| PATCH | /api/v1/admin/tags/{tagId}/status |
태그 활성화/비활성화 | ADMIN | ❌ 구현 필요 |
| DELETE | /api/v1/admin/tags/{tagId} |
태그 삭제 | SUPER_ADMIN | ❌ 구현 필요 |
📊 통계
총 API 개수: 15개
✅ 완료: 0개
❌ 구현 필요: 15개
Part별 개수:
- Part 1 (Tag 조회): 3개 (전체 필요)
- Part 2 (Member 선호 태그): 4개 (전체 필요)
- Part 3 (Institution 태그): 1개 (필요)
- Part 4 (Tag 관리): 5개 (전체 필요)
✅ API 우선순위
1순위 (핵심 기능)
GET /api/v1/tags/category/{category}- 카테고리별 태그 조회PUT /api/v1/members/me/preference-tags- 선호 태그 업데이트PUT /api/v1/institutions/{institutionId}/tags- 기관 태그 업데이트
2순위 (관리 기능)
POST /api/v1/admin/tags- 태그 생성PUT /api/v1/admin/tags/{tagId}- 태그 수정PATCH /api/v1/admin/tags/{tagId}/status- 활성화/비활성화
3순위 (부가 기능)
- 선호 태그 개별 추가/제거
- 태그 삭제
🔍 현재 상태 분석
✅ 이미 존재하는 것
Tag Entity
✅ /domain/tag/entity/Tag.java
- id, category, code, name, description
- isActive, displayOrder
TagCategory Enum
✅ /domain/tag/entity/TagCategory.java
- SPECIALIZATION (전문/질환)
- SERVICE (서비스 유형)
- OPERATION (운영 특성)
- ENVIRONMENT (환경/시설)
- REVIEW (리뷰 유형)
ReviewTagMapping
✅ /domain/tag/entity/ReviewTagMapping.java
- Review ↔ Tag 연결
- Unique: (review_id, tag_id)
❌ 구현 필요한 것
InstitutionTagMapping
❌ 신규 생성 필요
- Institution ↔ Tag 연결
- Unique: (institution_id, tag_id)
MemberPreferenceTagMapping
❌ 신규 생성 필요
- Member ↔ Tag 연결
- Unique: (member_id, tag_id)
- 용도: AI 추천용 선호 태그
제거 필요한 Enum들
❌ SpecializedCondition (Institution용, 3개 항목)
❌ SpecializationTag (15개 항목)
❌ ServiceTag, OperationTag, EnvironmentTag, ReviewTag
🔗 Part 1: Tag 조회 (공개)
구현 필요 (❌)
1. 전체 태그 목록
GET /api/v1/tags
권한: 공개
필터: isActive=true (기본값)
응답: 모든 카테고리의 태그 목록
2. 카테고리별 태그 목록
GET /api/v1/tags/category/{category}
권한: 공개
Path Variable: SPECIALIZATION, SERVICE, OPERATION, ENVIRONMENT, REVIEW
필터: isActive=true (활성화된 것만)
정렬: displayOrder 오름차순
응답:
- tagId, code, name, category, displayOrder
용도: 사용자가 선호 태그 선택할 때 사용
3. 태그 상세 조회
GET /api/v1/tags/{tagId}
권한: 공개
응답: tagId, code, name, category, description, isActive, displayOrder
⭐ Part 2: Member 선호 태그
구현 필요 (❌)
Member 선호 태그란?
- Member가 원하는 조건을 태그로 저장
- AI 추천 시스템에서 활용하여 매칭률 계산
- 예: "치매 전문", "24시간 운영", "식사 제공", "주차 가능"
1. 내 선호 태그 조회
GET /api/v1/members/me/preference-tags
권한: USER
응답:
- 내가 선택한 태그 목록
- 카테고리별로 그룹화
예시:
{
"SPECIALIZATION": [{"id": 1, "name": "치매 전문"}],
"SERVICE": [{"id": 5, "name": "식사 제공"}],
"OPERATION": [{"id": 10, "name": "24시간 운영"}]
}
2. 선호 태그 일괄 업데이트
PUT /api/v1/members/me/preference-tags
권한: USER
입력: { "tagIds": [1, 3, 5, 8, 10] }
동작:
- 기존 선호 태그 모두 제거
- 새 태그 일괄 추가
- 중복 방지 (Unique 제약)
검증:
- 태그 ID가 유효한지 확인
- 최대 20개까지 선택 가능
3. 선호 태그 추가
POST /api/v1/members/me/preference-tags/{tagId}
권한: USER
동작: 선호 태그에 추가
검증: 중복 추가 방지
4. 선호 태그 제거
DELETE /api/v1/members/me/preference-tags/{tagId}
권한: USER
동작: 선호 태그에서 제거
🏥 Part 3: Institution 태그 연결
구현 필요 (❌)
기관 태그 업데이트
PUT /api/v1/institutions/{institutionId}/tags
권한: OWNER, MANAGER
입력: { "tagIds": [1, 2, 5, 8, 11] }
동작:
- 기존 기관 태그 모두 제거
- 새 태그 일괄 추가
- InstitutionTagMapping에 저장
검증:
- 본인 기관만 수정 가능
- 카테고리 제한 (REVIEW 제외)
🔧 Part 4: Tag 관리 (관리자)
구현 필요 (❌)
1. 전체 태그 관리 목록
GET /api/v1/admin/tags
권한: ADMIN
필터: category, isActive
페이징: page, size
정렬: category, displayOrder
응답: 비활성화된 태그 포함 전체 조회
2. 태그 생성
POST /api/v1/admin/tags
권한: ADMIN
입력:
- category: TagCategory (필수)
- code: 태그 코드 (대문자, 언더스코어, 필수)
- name: 태그명 (한글, 필수)
- description: 설명 (선택)
- displayOrder: 정렬 순서 (선택)
검증:
- code 중복 체크 (Unique)
- code 형식 검증 (^[A-Z_]+$)
3. 태그 수정
PUT /api/v1/admin/tags/{tagId}
권한: ADMIN
수정 가능: name, description, displayOrder
수정 불가: category, code (데이터 일관성)
4. 태그 활성화/비활성화
PATCH /api/v1/admin/tags/{tagId}/status?isActive=false
권한: ADMIN
동작: isActive 변경 (삭제 대신 비활성화)
5. 태그 삭제
DELETE /api/v1/admin/tags/{tagId}
권한: SUPER_ADMIN
조건: 사용 중이지 않은 태그만 삭제 가능
검증:
- InstitutionTagMapping에서 사용 여부 확인
- MemberPreferenceTagMapping에서 사용 여부 확인
- ReviewTagMapping에서 사용 여부 확인
🏗️ 엔티티 구조
Tag (태그)
✅ 이미 존재
- id, category, code, name
- description, isActive, displayOrder
TagCategory (Enum)
✅ 이미 존재
- SPECIALIZATION, SERVICE, OPERATION
- ENVIRONMENT, REVIEW
ReviewTagMapping
✅ 이미 존재
- id, review, tag
InstitutionTagMapping
❌ 신규 생성 필요
@Entity
@Table(uniqueConstraints = {
@UniqueConstraint(columnNames = {"institution_id", "tag_id"})
})
public class InstitutionTagMapping extends BaseEntity {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "institution_id", nullable = false)
private Institution institution;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "tag_id", nullable = false)
private Tag tag;
}
MemberPreferenceTagMapping
❌ 신규 생성 필요
@Entity
@Table(uniqueConstraints = {
@UniqueConstraint(columnNames = {"member_id", "tag_id"})
})
public class MemberPreferenceTagMapping extends BaseEntity {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "tag_id", nullable = false)
private Tag tag;
}
Institution 수정
🔧 연관관계 추가 필요
@OneToMany(mappedBy = "institution", cascade = CascadeType.ALL)
private List<InstitutionTagMapping> tagMappings = new ArrayList<>();
Member 수정
🔧 연관관계 추가 필요
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
private List<MemberPreferenceTagMapping> preferenceTags = new ArrayList<>();
📋 구현 순서
Step 1: Mapping Entity 생성 (1일)
- InstitutionTagMapping Entity 생성
- InstitutionTagMappingRepository 생성
- MemberPreferenceTagMapping Entity 생성
- MemberPreferenceTagMappingRepository 생성
- Institution, Member Entity 수정 (연관관계 추가)
Step 2: Tag 초기 데이터 INSERT (1일)
2-1. SQL 파일 작성
src/main/resources/data/initial-tags.sql
-- ====================================
-- Tag 초기 데이터 INSERT
-- ====================================
-- SPECIALIZATION (전문/질환) 카테고리
INSERT INTO tag (category, code, name, description, is_active, display_order, created_at, updated_at)
VALUES
('SPECIALIZATION', 'DEMENTIA', '치매', '알츠하이머 및 치매 전문 케어', true, 1, NOW(), NOW()),
('SPECIALIZATION', 'STROKE', '뇌졸중', '뇌졸중 재활 케어', true, 2, NOW(), NOW()),
('SPECIALIZATION', 'PARKINSONS', '파킨슨병', '파킨슨병 전문 케어', true, 3, NOW(), NOW()),
('SPECIALIZATION', 'DIABETES', '당뇨', '당뇨 관리 케어', true, 4, NOW(), NOW()),
('SPECIALIZATION', 'HYPERTENSION', '고혈압', '고혈압 관리', true, 5, NOW(), NOW()),
('SPECIALIZATION', 'ARTHRITIS', '관절염', '관절염 케어', true, 6, NOW(), NOW()),
('SPECIALIZATION', 'CANCER', '암', '암 환자 케어', true, 7, NOW(), NOW()),
('SPECIALIZATION', 'REHABILITATION', '재활', '재활 치료 전문', true, 8, NOW(), NOW()),
('SPECIALIZATION', 'RESPIRATORY', '호흡기 질환', '호흡기 질환 케어', true, 9, NOW(), NOW()),
('SPECIALIZATION', 'HEART_DISEASE', '심장 질환', '심장 질환 케어', true, 10, NOW(), NOW());
-- SERVICE (서비스 유형) 카테고리
INSERT INTO tag (category, code, name, description, is_active, display_order, created_at, updated_at)
VALUES
('SERVICE', 'MEAL', '식사 제공', '영양 관리 식단 제공', true, 1, NOW(), NOW()),
('SERVICE', 'BATHING', '목욕 서비스', '전문 목욕 케어', true, 2, NOW(), NOW()),
('SERVICE', 'EXERCISE', '운동 프로그램', '신체 활동 프로그램', true, 3, NOW(), NOW()),
('SERVICE', 'PHYSICAL_THERAPY', '물리치료', '전문 물리치료 서비스', true, 4, NOW(), NOW()),
('SERVICE', 'OCCUPATIONAL_THERAPY', '작업치료', '일상생활 훈련', true, 5, NOW(), NOW()),
('SERVICE', 'COGNITIVE_PROGRAM', '인지 프로그램', '두뇌 활동 프로그램', true, 6, NOW(), NOW()),
('SERVICE', 'MEDICAL_CHECKUP', '건강 검진', '정기 건강 검진', true, 7, NOW(), NOW()),
('SERVICE', 'MEDICATION_MANAGEMENT', '약물 관리', '복약 관리 서비스', true, 8, NOW(), NOW()),
('SERVICE', 'EMERGENCY_CARE', '응급 케어', '응급 상황 대응', true, 9, NOW(), NOW()),
('SERVICE', 'COUNSELING', '심리 상담', '심리 상담 서비스', true, 10, NOW(), NOW());
-- OPERATION (운영 특성) 카테고리
INSERT INTO tag (category, code, name, description, is_active, display_order, created_at, updated_at)
VALUES
('OPERATION', '24H_OPERATION', '24시간 운영', '24시간 케어 제공', true, 1, NOW(), NOW()),
('OPERATION', 'NURSE_STATIONED', '간호사 상주', '간호사 24시간 상주', true, 2, NOW(), NOW()),
('OPERATION', 'DOCTOR_VISIT', '의사 정기 방문', '의사 정기 방문 진료', true, 3, NOW(), NOW()),
('OPERATION', 'FAMILY_VISIT', '가족 면회 자유', '가족 면회 제한 없음', true, 4, NOW(), NOW()),
('OPERATION', 'EMERGENCY_SYSTEM', '응급 시스템', '응급 호출 시스템 완비', true, 5, NOW(), NOW()),
('OPERATION', 'CCTV_MONITORING', 'CCTV 모니터링', '24시간 CCTV 모니터링', true, 6, NOW(), NOW()),
('OPERATION', 'SMALL_SCALE', '소규모 운영', '가정집 같은 분위기', true, 7, NOW(), NOW()),
('OPERATION', 'LARGE_SCALE', '대규모 시설', '종합 케어 시설', true, 8, NOW(), NOW());
-- ENVIRONMENT (환경/시설) 카테고리
INSERT INTO tag (category, code, name, description, is_active, display_order, created_at, updated_at)
VALUES
('ENVIRONMENT', 'PARKING', '주차 가능', '주차 공간 완비', true, 1, NOW(), NOW()),
('ENVIRONMENT', 'ACCESSIBLE', '장애인 편의시설', '휠체어 접근 가능', true, 2, NOW(), NOW()),
('ENVIRONMENT', 'PRIVATE_ROOM', '개인실 가능', '개인 공간 제공', true, 3, NOW(), NOW()),
('ENVIRONMENT', 'SHARED_ROOM', '다인실', '경제적인 다인실', true, 4, NOW(), NOW()),
('ENVIRONMENT', 'GARDEN', '정원', '산책 가능한 정원', true, 5, NOW(), NOW()),
('ENVIRONMENT', 'EXERCISE_ROOM', '운동실', '실내 운동 시설', true, 6, NOW(), NOW()),
('ENVIRONMENT', 'DINING_HALL', '식당', '공동 식당 운영', true, 7, NOW(), NOW()),
('ENVIRONMENT', 'LOUNGE', '휴게실', '휴게 공간 완비', true, 8, NOW(), NOW()),
('ENVIRONMENT', 'AIR_CONDITIONING', '냉난방', '쾌적한 실내 온도', true, 9, NOW(), NOW()),
('ENVIRONMENT', 'CLEAN_FACILITY', '청결한 시설', '깨끗한 환경 유지', true, 10, NOW(), NOW());
-- REVIEW (리뷰 유형) 카테고리
INSERT INTO tag (category, code, name, description, is_active, display_order, created_at, updated_at)
VALUES
('REVIEW', 'KIND', '친절함', '직원이 친절해요', true, 1, NOW(), NOW()),
('REVIEW', 'CLEAN', '청결함', '시설이 깨끗해요', true, 2, NOW(), NOW()),
('REVIEW', 'PROFESSIONAL', '전문성', '전문적인 케어를 받았어요', true, 3, NOW(), NOW()),
('REVIEW', 'COMFORTABLE', '편안함', '편안한 환경이에요', true, 4, NOW(), NOW()),
('REVIEW', 'GOOD_MEAL', '좋은 식사', '식사가 맛있어요', true, 5, NOW(), NOW()),
('REVIEW', 'SAFE', '안전함', '안전하게 관리되고 있어요', true, 6, NOW(), NOW()),
('REVIEW', 'COMMUNICATION', '소통 원활', '가족과 소통이 잘 돼요', true, 7, NOW(), NOW()),
('REVIEW', 'VALUE_FOR_MONEY', '가성비', '가격 대비 만족스러워요', true, 8, NOW(), NOW()),
('REVIEW', 'RECOMMEND', '추천함', '다른 분께도 추천해요', true, 9, NOW(), NOW()),
('REVIEW', 'IMPROVEMENT_NEEDED', '개선 필요', '개선이 필요해요', true, 10, NOW(), NOW());2-2. data.sql 파일 위치
파일 생성: src/main/resources/data.sql
2-3. application.yml 설정
개발 환경 (application-dev.yml)
spring:
jpa:
hibernate:
ddl-auto: update # 또는 create-drop
defer-datasource-initialization: true # JPA 초기화 후 data.sql 실행
sql:
init:
mode: always # 항상 data.sql 실행
encoding: UTF-8 # 한글 깨짐 방지테스트 환경 (application-test.yml)
spring:
jpa:
hibernate:
ddl-auto: create-drop
defer-datasource-initialization: true
sql:
init:
mode: always
encoding: UTF-8운영 환경 (application-prod.yml)
spring:
jpa:
hibernate:
ddl-auto: validate # 변경 금지
defer-datasource-initialization: false
sql:
init:
mode: never # 운영 환경에서는 data.sql 실행 안 함2-4. 실행 방법
-
data.sql 파일 생성
- 위치:
src/main/resources/data.sql - 2-1의 SQL 내용 복사
- 위치:
-
application-dev.yml 설정 확인
defer-datasource-initialization: true추가mode: always설정
-
애플리케이션 실행
./gradlew bootRun --args='--spring.profiles.active=dev' -
로그 확인
Executing SQL script from URL [file:.../data.sql] 48 row(s) affected
2-5. 검증 쿼리
-- 카테고리별 개수 확인
SELECT category, COUNT(*) as count
FROM tag
GROUP BY category
ORDER BY category;
-- 예상 결과:
-- SPECIALIZATION: 10개
-- SERVICE: 10개
-- OPERATION: 8개
-- ENVIRONMENT: 10개
-- REVIEW: 10개
-- 총: 48개
-- 전체 태그 확인
SELECT id, category, code, name, is_active, display_order
FROM tag
ORDER BY category, display_order;
-- code 중복 확인 (0이어야 함)
SELECT code, COUNT(*) as count
FROM tag
GROUP BY code
HAVING COUNT(*) > 1;2-6. 체크리스트
- src/main/resources/data.sql 파일 생성
- 2-1의 SQL 내용 복사
- application-dev.yml에 설정 추가
-
defer-datasource-initialization: true -
mode: always -
encoding: UTF-8
-
- 애플리케이션 실행
- 로그에서 "48 row(s) affected" 확인
- DB에서 검증 쿼리 실행
- code 중복 없는지 확인 (0건이어야 함)
- 모든 카테고리가 있는지 확인 (5개 카테고리, 총 48개)
Step 3: Tag 공개 API (1일)
- TagService 구현
- getActiveTags()
- getTagsByCategory()
- getTagById()
- TagController 구현 (3개 API)
Step 4: Member 선호 태그 (2일)
- MemberPreferenceService 구현
- getMyPreferenceTags()
- updatePreferenceTags()
- addPreferenceTag()
- removePreferenceTag()
- MemberPreferenceController 구현 (4개 API)
- 최대 20개 제한 검증
Step 5: Institution 태그 업데이트 (1일)
- InstitutionService에 updateTags() 추가
- InstitutionController에 PUT /tags 추가
- 카테고리 제한 검증 (REVIEW 제외)
Step 6: Tag 관리 API (관리자) (2일)
- TagAdminService 구현
- getAllTags()
- createTag()
- updateTag()
- toggleStatus()
- deleteTag()
- TagAdminController 구현 (5개 API)
- code 중복 검증, 형식 검증
Step 7: 데이터 마이그레이션 (선택적) (1일)
- InstitutionSpecializedCondition → InstitutionTagMapping
- 마이그레이션 SQL 작성
- 데이터 검증
Step 8: Enum 제거 (선택적) (1일)
- SpecializedCondition 제거
- SpecializationTag, ServiceTag 등 제거
- InstitutionSpecializedCondition Entity 제거
Step 9: 테스트 (1일)
- Swagger 테스트
- 권한 체크 검증
- 중복 검증 확인
예상 기간: 11일 (약 2.5주)
🚨 핵심 주의사항
-
선호 태그 용도
- Member가 원하는 조건 저장 (AI 추천용)
- 예: "치매 전문", "24시간 운영", "식사 제공"
-
카테고리 제한
- Institution: SPECIALIZATION, SERVICE, OPERATION, ENVIRONMENT 가능
- Member: 모든 카테고리 가능
- Review: REVIEW 카테고리만 가능
-
삭제 불가
- 사용 중인 태그는 삭제 불가 (비활성화만)
- Institution, Member, Review 중 하나라도 사용 중이면 삭제 불가
-
중복 방지
- Unique 제약: (institution_id, tag_id)
- Unique 제약: (member_id, tag_id)
- 중복 추가 시 예외 처리
-
AI 추천 연동
- Member 선호 태그를 텍스트로 변환하여 AI 서버에 전달
- 기관 태그와 매칭률 계산하여 추천 순위 결정
💡 사용 시나리오
1. 회원가입 후 선호 조건 선택
"어떤 요양 기관을 찾으시나요?"
전문 분야:
□ 치매 전문 □ 뇌졸중 재활 □ 파킨슨병
서비스:
□ 식사 제공 □ 목욕 서비스 □ 운동 프로그램
운영:
□ 24시간 운영 □ 가족 면회 자유
환경:
□ 주차 가능 □ 장애인 편의시설
→ 선택 → MemberPreferenceTagMapping에 저장
2. AI 추천 시 매칭
사용자 선호: DEMENTIA, MEAL, 24H_OPERATION, PARKING
기관 A 태그: DEMENTIA ✅ MEAL ✅ 24H_OPERATION ✅ PARKING ✅
→ 매칭률 100% → 1순위!
기관 B 태그: DEMENTIA ✅ MEAL ✅
→ 매칭률 50% → 2순위
📚 참고 파일
이미 존재하는 파일
✅ /domain/tag/entity/Tag.java
✅ /domain/tag/entity/TagCategory.java
✅ /domain/tag/entity/ReviewTagMapping.java
✅ /domain/tag/repository/TagRepository.java
✅ /domain/tag/service/TagService.java
생성 필요한 파일
❌ /domain/tag/entity/InstitutionTagMapping.java
❌ /domain/tag/entity/MemberPreferenceTagMapping.java
❌ /domain/tag/repository/InstitutionTagMappingRepository.java
❌ /domain/tag/repository/MemberPreferenceTagMappingRepository.java
❌ /domain/tag/service/MemberPreferenceService.java
❌ /api/controller/TagController.java
❌ /api/controller/MemberPreferenceController.java
❌ /api/admin/controller/TagAdminController.java
❌ /api/dto/tag/TagResponseDto.java
❌ /api/dto/tag/PreferenceTagResponseDto.java
❌ /api/dto/tag/TagCreateRequestDto.java
수정 필요한 파일
🔧 /domain/institution/profile/entity/Institution.java (tagMappings 추가)
🔧 /domain/user/guardian/entity/Member.java (preferenceTags 추가)
🔧 /domain/institution/profile/service/InstitutionService.java (updateTags 추가)
💡 구현 팁
1. Tag 조회 (캐싱)
@Cacheable(value = "tags", key = "#category")
public List<TagResponseDto> getTagsByCategory(TagCategory category) {
List<Tag> tags = tagRepository
.findByCategoryAndIsActiveTrueOrderByDisplayOrderAsc(category);
return tags.stream().map(TagResponseDto::from).toList();
}2. 선호 태그 업데이트
@Transactional
public void updatePreferenceTags(Long memberId, List<Long> tagIds) {
// 최대 20개 제한
if (tagIds.size() > 20) {
throw new BusinessException(ErrorCode.TOO_MANY_PREFERENCE_TAGS);
}
// 기존 태그 모두 제거
memberPreferenceRepository.deleteByMemberId(memberId);
// 새 태그 추가
List<Tag> tags = tagRepository.findAllById(tagIds);
Member member = memberRepository.getReferenceById(memberId);
List<MemberPreferenceTagMapping> mappings = tags.stream()
.map(tag -> MemberPreferenceTagMapping.builder()
.member(member)
.tag(tag)
.build())
.toList();
memberPreferenceRepository.saveAll(mappings);
}3. 기관 태그 업데이트
@Transactional
public void updateInstitutionTags(Long institutionId, Long adminId, List<Long> tagIds) {
// 권한 체크
if (!securityService.canManage(institutionId, adminId)) {
throw new BusinessException(ErrorCode.FORBIDDEN);
}
Institution institution = findById(institutionId);
// 기존 태그 제거
institution.getTagMappings().clear();
// 새 태그 추가
List<Tag> tags = tagRepository.findAllById(tagIds);
for (Tag tag : tags) {
// REVIEW 카테고리 제외
if (tag.getCategory() == TagCategory.REVIEW) {
throw new BusinessException(ErrorCode.INVALID_TAG_CATEGORY);
}
institution.addTag(tag);
}
}4. 태그 삭제 전 사용 여부 확인
public void deleteTag(Long tagId) {
Tag tag = findById(tagId);
// 사용 중인 태그는 삭제 불가
if (isTagInUse(tagId)) {
throw new BusinessException(ErrorCode.TAG_IN_USE_CANNOT_DELETE);
}
tagRepository.delete(tag);
}
private boolean isTagInUse(Long tagId) {
return institutionTagMappingRepository.existsByTagId(tagId)
|| memberPreferenceRepository.existsByTagId(tagId)
|| reviewTagMappingRepository.existsByTagId(tagId);
}🎯 완료 기준
- 모든 API가 Swagger에서 정상 동작
- 카테고리별 태그 조회가 정상 동작함
- Member 선호 태그 추가/조회가 정상 동작함
- Institution 태그 업데이트가 정상 동작함
- 관리자 태그 관리가 정상 동작함
- 중복 추가가 방지됨
- 사용 중인 태그는 삭제 불가
- 최대 20개 제한이 동작함