From e231c1f452c8f4237580db984e8a5ea062713591 Mon Sep 17 00:00:00 2001 From: Doi Kim <154426332+d0ikim@users.noreply.github.com> Date: Sat, 4 Jul 2026 15:39:59 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EA=B3=B5=EC=97=B0=EB=93=B1=EB=A1=9D/=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=8B=9C=20=EC=B6=9C=EC=97=B0=EB=9D=BC=EC=9D=B8=EC=97=85(music?= =?UTF-8?q?ianIds)=EC=A7=80=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +-- .../again/domain/admin/AdminController.java | 26 ++++++++++--- .../PerformanceLineupRepository.java | 11 ++++++ .../performance/PerformanceRequest.java | 2 + .../performance/PerformanceService.java | 39 ++++++++++++++++++- 5 files changed, 74 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 7dbcf0c..ef9bafb 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ - **뮤지션 인맥지도** — 함께 공연한 협연자와 협연 횟수를 그래프로 시각화 - **카카오 소셜 로그인 + JWT 인증** — GENERAL / MUSICIAN / ADMIN 3가지 유저 역할 - **뮤지션 인증 플로우** — 일반 유저의 뮤지션 인증 신청 → 어드민 승인/거부 -- **어드민 관리** — 뮤지션·공연장·공연 등록/수정, 뮤지션 인증 대기 목록 관리 +- **어드민 관리** — 뮤지션·공연장·공연 등록/수정(출연 라인업 지정 포함), 뮤지션 인증 대기 목록 관리 - **공연장 좌표 크롤링** — Kakao Local API로 기존 공연장 주소를 위도/경도로 변환해 저장 --- @@ -73,8 +73,8 @@ | GET | `/api/performances/search?type=genre&keyword=` | 장르 검색 | 불필요 | - | | GET | `/api/performances/musician/{id}` | 특정 뮤지션의 공연 목록 | 불필요 | ✓ | | POST | `/api/performances` | 공연 이력 추가 | 필요 (MUSICIAN) | ✓ | -| POST | `/api/admin/performances` | 공연 등록 | 필요 (ADMIN) | ✓ | -| PUT | `/api/admin/performances/{id}` | 공연 수정 (취소/복구) | 필요 (ADMIN) | ✓ | +| POST | `/api/admin/performances` | 공연 등록 (라인업 포함 가능) | 필요 (ADMIN) | ✓ | +| PUT | `/api/admin/performances/{id}` | 공연 수정 (취소/복구, 라인업 교체) | 필요 (ADMIN) | ✓ | ### 어드민 | Method | URL | 설명 | 인증 | 프론트 | diff --git a/backend/src/main/java/com/dazz/again/domain/admin/AdminController.java b/backend/src/main/java/com/dazz/again/domain/admin/AdminController.java index 7d45bc8..2a3c0cc 100644 --- a/backend/src/main/java/com/dazz/again/domain/admin/AdminController.java +++ b/backend/src/main/java/com/dazz/again/domain/admin/AdminController.java @@ -157,36 +157,50 @@ public ResponseEntity updateVenue(@PathVariable Long id, @RequestBody VenueRe @Operation( summary = "공연 등록", - description = "새 공연을 등록합니다. ADMIN만 호출 가능.", + description = """ + 새 공연을 등록합니다. ADMIN만 호출 가능. + + **musicianIds (라인업)** + - 출연 뮤지션 id 목록을 함께 보내면 공연 저장 후 라인업(performance_lineup)에도 등록됩니다. + - 생략(null)하면 라인업 없이 공연만 등록됩니다. + - 존재하지 않는 뮤지션 id가 섞여 있으면 공연 등록까지 전부 취소(롤백)됩니다. + """, security = @SecurityRequirement(name = "bearerAuth") ) @ApiResponse(responseCode = "200", description = "등록 성공") - @ApiResponse(responseCode = "400", description = "존재하지 않는 공연장 id") + @ApiResponse(responseCode = "400", description = "존재하지 않는 공연장 또는 뮤지션 id") @PostMapping("/performances") public ResponseEntity createPerformance(@RequestBody PerformanceRequest request) { // 요청 바디의 JSON을 PerformanceRequest 객체로 받아 PerformanceService에 등록 요청 try { return ResponseEntity.ok(performanceService.create(request)); } catch (NoSuchElementException e) { - // 요청의 venueId에 해당하는 공연장이 없으면 400 반환 + // 요청의 venueId에 해당하는 공연장이 없거나 musicianIds에 없는 뮤지션이 있으면 400 반환 return ResponseEntity.badRequest().body(e.getMessage()); } } @Operation( summary = "공연 수정", - description = "기존 공연 정보를 수정합니다. ADMIN만 호출 가능.", + description = """ + 기존 공연 정보를 수정합니다. ADMIN만 호출 가능. + + **musicianIds (라인업)** + - 목록을 보내면 기존 라인업을 전부 지우고 보낸 목록으로 교체합니다. + - 빈 배열([])을 보내면 라인업이 전부 삭제됩니다. + - 생략(null)하면 라인업은 건드리지 않고 공연 정보만 수정됩니다. + """, security = @SecurityRequirement(name = "bearerAuth") ) @ApiResponse(responseCode = "200", description = "수정 성공") - @ApiResponse(responseCode = "400", description = "존재하지 않는 공연 또는 공연장 id") + @ApiResponse(responseCode = "400", description = "존재하지 않는 공연, 공연장 또는 뮤지션 id") @PutMapping("/performances/{id}") public ResponseEntity updatePerformance(@PathVariable Long id, @RequestBody PerformanceRequest request) { // {id}로 수정할 공연을 특정하고, 요청 바디로 새 값을 받아 PerformanceService에 수정 요청 try { return ResponseEntity.ok(performanceService.update(id, request)); } catch (NoSuchElementException e) { - // 존재하지 않는 공연 또는 공연장 id면 400 반환 + // 존재하지 않는 공연, 공연장 또는 뮤지션 id면 400 반환 return ResponseEntity.badRequest().body(e.getMessage()); } } diff --git a/backend/src/main/java/com/dazz/again/domain/performance/PerformanceLineupRepository.java b/backend/src/main/java/com/dazz/again/domain/performance/PerformanceLineupRepository.java index 36aaae8..917e7e0 100644 --- a/backend/src/main/java/com/dazz/again/domain/performance/PerformanceLineupRepository.java +++ b/backend/src/main/java/com/dazz/again/domain/performance/PerformanceLineupRepository.java @@ -43,4 +43,15 @@ WHERE pl.performance IN ( ORDER BY COUNT(pl.performance) DESC """) List findCoPerformers(@Param("musicianId") Long musicianId); + + // 특정 공연의 라인업을 전부 삭제하는 메서드 (공연 수정 시 라인업 교체용) + // + // 메서드 이름 규칙(쿼리 메서드)으로 Spring Data JPA가 쿼리를 자동 생성함: + // deleteBy + Id(복합키 필드명) + _(언더스코어: 복합키 안으로 한 단계 들어감) + PerformanceId + // → "복합키(id) 안의 performanceId가 일치하는 행을 모두 DELETE" + // 실행되는 SQL: DELETE FROM performance_lineup WHERE performance_id = ? + // + // 삭제는 DB를 변경하는 작업이라 트랜잭션 안에서만 실행 가능 + // → 호출하는 쪽(PerformanceService의 메서드)에 @Transactional이 붙어 있어야 함 + void deleteById_PerformanceId(Long performanceId); } diff --git a/backend/src/main/java/com/dazz/again/domain/performance/PerformanceRequest.java b/backend/src/main/java/com/dazz/again/domain/performance/PerformanceRequest.java index d9f3b7c..6645070 100644 --- a/backend/src/main/java/com/dazz/again/domain/performance/PerformanceRequest.java +++ b/backend/src/main/java/com/dazz/again/domain/performance/PerformanceRequest.java @@ -5,6 +5,7 @@ import lombok.NoArgsConstructor; // 파라미터 없는 기본 생성자 자동 생성 — Spring이 JSON을 이 객체로 변환할 때 필요 import java.time.LocalDateTime; // 공연 일시를 담기 위한 날짜+시각 타입 +import java.util.List; // 출연 뮤지션 id를 여러 개 담기 위한 컬렉션 타입 // @Entity 없음 — DB 테이블이 아니라 요청 데이터를 잠시 담는 그릇(DTO) @Getter @@ -19,4 +20,5 @@ public class PerformanceRequest { private String setList; // 셋리스트 (선택, 곡명 콤마 구분) private boolean cancelled; // 취소 여부 (필수, 기본값 false) private String sourceUrl; // 출처 URL (선택) + private List musicianIds; // 출연 뮤지션 id 목록 (선택) — null이면 라인업을 건드리지 않고, 값이 있으면 라인업을 이 목록으로 통째로 교체 } \ No newline at end of file diff --git a/backend/src/main/java/com/dazz/again/domain/performance/PerformanceService.java b/backend/src/main/java/com/dazz/again/domain/performance/PerformanceService.java index 378dde1..4724782 100644 --- a/backend/src/main/java/com/dazz/again/domain/performance/PerformanceService.java +++ b/backend/src/main/java/com/dazz/again/domain/performance/PerformanceService.java @@ -106,7 +106,12 @@ public Performance create(PerformanceRequest request) { .sourceUrl(request.getSourceUrl()) // 출처 URL (null 가능) .build(); // 객체 생성 완료 - return performanceRepository.save(performance); // INSERT INTO performance ... — 새 객체라 save() 필요 + performanceRepository.save(performance); // INSERT INTO performance ... — 여기서 performance.id가 채워짐 + + // 요청에 musicianIds가 있으면 출연 뮤지션들을 라인업에 등록 (null이면 라인업 없이 공연만 저장) + saveLineup(performance, request.getMusicianIds()); + + return performance; // 저장된 공연 정보 반환 } // ADMIN 전용 — 공연 수정 — id로 기존 공연을 찾아 update() 호출 @@ -128,6 +133,38 @@ public Performance update(Long id, PerformanceRequest request) { request.isCancelled(), // 취소 여부 request.getSourceUrl() // 출처 URL ); // save() 없음 — @Transactional + 더티 체킹으로 트랜잭션 종료 시 UPDATE 자동 실행 + + // 요청에 musicianIds가 있으면 기존 라인업을 전부 지우고 새 목록으로 교체 + // (null이면 라인업은 손대지 않음 — 공연 정보만 수정하고 싶을 때를 위해) + if (request.getMusicianIds() != null) { + performanceLineupRepository.deleteById_PerformanceId(performance.getId()); // 기존 라인업 전체 삭제 + performanceLineupRepository.flush(); // DELETE를 즉시 DB에 반영 — JPA는 기본적으로 INSERT를 DELETE보다 먼저 내보내서, + // flush 없이 같은 뮤지션을 다시 넣으면 "이미 존재하는 PK" 충돌이 날 수 있음 + saveLineup(performance, request.getMusicianIds()); // 새 목록으로 다시 등록 + } + return performance; // 수정된 엔티티 반환 } + + // 공통 로직 — 뮤지션 id 목록을 라인업(performance_lineup)에 저장 + // create()와 update() 양쪽에서 호출하므로 별도 메서드로 분리 + private void saveLineup(Performance performance, List musicianIds) { + if (musicianIds == null) { + return; // musicianIds를 아예 안 보낸 경우 — 라인업 저장 생략 + } + + for (Long musicianId : musicianIds) { + // 존재하지 않는 뮤지션 id가 섞여 있으면 예외 발생 + // → @Transactional 덕분에 이미 저장된 공연/라인업까지 전부 롤백됨 (전체 성공 아니면 전체 취소) + Musician musician = musicianRepository.findById(musicianId) + .orElseThrow(() -> new NoSuchElementException("존재하지 않는 뮤지션입니다. id=" + musicianId)); + + PerformanceLineup lineup = PerformanceLineup.builder() + .id(new PerformanceLineupId(performance.getId(), musician.getId())) // 복합키(공연id, 뮤지션id) 생성 + .performance(performance) // 공연 객체 + .musician(musician) // 뮤지션 객체 + .build(); + performanceLineupRepository.save(lineup); // performance_lineup 테이블에 INSERT + } + } } \ No newline at end of file From 0e7dea4b84310bce031622073507c6a7597cfda6 Mon Sep 17 00:00:00 2001 From: Doi Kim <154426332+d0ikim@users.noreply.github.com> Date: Sat, 4 Jul 2026 15:47:47 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20application.yaml=EC=97=90=EC=84=9C?= =?UTF-8?q?=20sever=EB=B8=94=EB=A1=9D=EC=9D=B4=20spring=EB=B8=94=EB=A1=9D?= =?UTF-8?q?=EC=9D=84=20=EC=B9=A8=EB=B2=94=ED=95=B4=20OAuth2=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=9D=B4=20=EC=9C=A0=EC=8B=A4=EB=90=98=EB=8D=98=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/application.yaml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index b8c53a5..48876d1 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -11,11 +11,6 @@ spring: password: ${DB_PASSWORD} # 비밀번호는 환경변수로 관리 (코드에 직접 쓰면 위험) driver-class-name: org.postgresql.Driver # PostgreSQL 드라이버 사용 -server: - # ${PORT:8080} : Render 같은 배포 환경은 PORT 환경변수로 "이 포트를 써라"라고 알려줌 - # 로컬에선 PORT를 안 정해놨으니 지금까지처럼 8080번 그대로 사용 - port: ${PORT:8080} - jpa: hibernate: ddl-auto: update # 서버 시작 시 @Entity 클래스 보고 테이블 자동 생성/변경 (데이터는 유지) @@ -45,6 +40,13 @@ server: user-info-uri: https://kapi.kakao.com/v2/user/me # 카카오에서 유저 정보 받아오는 URL user-name-attribute: id # 카카오 응답에서 유저 고유값 키 이름 +# 서버 설정 — spring: 블록과 같은 최상위 레벨에 있어야 함 +# (spring: 블록 안에 넣으면 jpa/security 설정의 소속이 바뀌어 서버가 못 뜸) +server: + # ${PORT:8080} : Render 같은 배포 환경은 PORT 환경변수로 "이 포트를 써라"라고 알려줌 + # 로컬에선 PORT를 안 정해놨으니 지금까지처럼 8080번 그대로 사용 + port: ${PORT:8080} + # JWT 설정 jwt: secret: ${JWT_SECRET} # JWT 서명용 비밀 키 (64자 이상 권장, HS512 자동 적용)