[
  {
    "path": ".claude/settings.local.json",
    "content": "{\n  \"permissions\": {\n    \"allow\": [\n      \"mcp__mysql__mysql_query\"\n    ]\n  }\n}\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "* @ImNM @sanbonai06 @cofls6581 @gengminy @kim-wonjin"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "## 개요\n- close #issueNumber\n\n## 작업사항\n- 내용을 적어주세요.\n\n## 변경로직\n- 내용을 적어주세요."
  },
  {
    "path": ".github/workflows/BuildApiServer.yml",
    "content": "name: Build Api Server\non:\n  push:\n    tags:\n      - Api-v*.*.*\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        java-version: [ 21 ]\n    outputs:\n      version: ${{ steps.get_version.outputs.BRANCH_NAME }}\n\n    steps:\n      - name: Check Out The Repository\n        uses: actions/checkout@v4\n\n      - name: Set up Java\n        uses: actions/setup-java@v4\n        with:\n          java-version: ${{ matrix.java-version }}\n          distribution: 'temurin'\n\n      - name: Get the version\n        id: get_version\n        run: |\n          RELEASE_VERSION_WITHOUT_V=\"$(cut -d'v' -f2 <<< ${GITHUB_REF#refs/*/})\"\n          echo \"VERSION=$RELEASE_VERSION_WITHOUT_V\" >> $GITHUB_OUTPUT\n\n      #테스트 수행용 도커 컴포즈\n      - name: Start containers\n        run: docker compose up -d\n\n      - name: Gradle Build\n        uses: gradle/gradle-build-action@v3\n\n      - name: Execute Gradle build\n        run: ./gradlew :DuDoong-Api:build --no-daemon\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      - name: Build and push\n        uses: docker/build-push-action@v5\n        with:\n          context: ./DuDoong-Api\n          push: true\n          tags: water0641/dudoong-api:${{ steps.get_version.outputs.VERSION }}\n"
  },
  {
    "path": ".github/workflows/BuildBatchServer.yml",
    "content": "name: Build Batch Server\non:\n  push:\n    tags:\n      - Batch-v*.*.*\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        java-version: [ 21 ]\n    outputs:\n      version: ${{ steps.get_version.outputs.BRANCH_NAME }}\n\n    steps:\n      - name: Check Out The Repository\n        uses: actions/checkout@v4\n\n      - name: Set up Java\n        uses: actions/setup-java@v4\n        with:\n          java-version: ${{ matrix.java-version }}\n          distribution: 'temurin'\n\n      - name: Get the version\n        id: get_version\n        run: |\n          RELEASE_VERSION_WITHOUT_V=\"$(cut -d'v' -f2 <<< ${GITHUB_REF#refs/*/})\"\n          echo \"VERSION=$RELEASE_VERSION_WITHOUT_V\" >> $GITHUB_OUTPUT\n\n      #테스트 수행용 도커 컴포즈\n      - name: Start containers\n        run: docker compose up -d\n\n      - name: Gradle Build\n        uses: gradle/gradle-build-action@v3\n\n      - name: Execute Gradle build\n        run: ./gradlew :DuDoong-Batch:build --no-daemon\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      - name: Build and push\n        uses: docker/build-push-action@v5\n        with:\n          context: ./DuDoong-Batch\n          push: true\n          tags: |\n            water0641/dudoong-batch:${{ steps.get_version.outputs.VERSION }}\n            water0641/dudoong-batch:latest\n"
  },
  {
    "path": ".github/workflows/BuildSocketServer.yml",
    "content": "name: Build Socket Server\non:\n  push:\n    tags:\n      - Socket-v*.*.*\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        java-version: [ 17 ]\n    outputs:\n      version: ${{ steps.get_version.outputs.BRANCH_NAME }}\n\n    steps:\n      - name: Check Out The Repository\n        uses: actions/checkout@v3\n\n      - name: Set up Java\n        uses: actions/setup-java@v3\n        with:\n          java-version: ${{ matrix.java-version }}\n          distribution: 'corretto'\n\n      - name: Get the version\n        id: get_version\n        run: |\n          RELEASE_VERSION_WITHOUT_V=\"$(cut -d'v' -f2 <<< ${GITHUB_REF#refs/*/})\"\n          echo ::set-output name=VERSION::$RELEASE_VERSION_WITHOUT_V \n\n      #테스트 수행용 도커 컴포즈\n      - name: Start containers\n        run: docker compose up -d\n\n      - name: Gradle Build\n        uses: gradle/gradle-build-action@v2\n\n      - name: Execute Gradle build\n        run: ./gradlew :DuDoong-Socket:build --no-daemon\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v2\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v2\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      - name: Build and push\n        uses: docker/build-push-action@v3\n        with:\n          context: ./DuDoong-Socket\n          push: true\n          tags: water0641/dudoong-socket:${{ steps.get_version.outputs.VERSION }}\n\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: ci\non:\n  pull_request:\n    branch: 'dev'\n\njobs:\n  ktlintCheck:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: SetUp JDK 21\n        uses: actions/setup-java@v4\n        with:\n          java-version: \"21\"\n          distribution: 'temurin'\n\n      - name: Gradle Caching\n        uses: actions/cache@v4\n        with:\n          path: |\n            ~/.gradle/caches\n            ~/.gradle/wrapper\n          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }}\n          restore-keys: |\n            ${{ runner.os }}-gradle-\n\n      - name: Grant execute permission for gradlew\n        run: chmod +x ./gradlew\n\n      - name: ktlint check\n        run: ./gradlew ktlintCheck\n"
  },
  {
    "path": ".gitignore",
    "content": "DuDoong-Domain/HELP.md\n.gradle\nbuild/\n!gradle/wrapper/gradle-wrapper.jar\n!**/src/main/**/build/\n!**/src/test/**/build/\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\nbin/\n!**/src/main/**/bin/\n!**/src/test/**/bin/\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\nout/\n!**/src/main/**/out/\n!**/src/test/**/out/\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\n\n### VS Code ###\n.vscode/\n.env\n.env.*\n\n*.xlsx\n\n**/src/main/generated/\n.omc/\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# DuDoong Backend - Claude 컨텍스트\n\n## 🎯 현재 진행 중인 작업\n\n**Java → Kotlin 전체 마이그레이션**\n\n마스터 트래킹 이슈: https://github.com/Gosrock/DuDoong-Backend/issues/582\n\n---\n\n## 🏗️ 프로젝트 아키텍처\n\n### 기술 스택\n- **언어**: Kotlin 1.9.22 (Java → Kotlin 마이그레이션 완료)\n- **프레임워크**: Spring Boot 3.2.0\n- **런타임**: Java 21\n- **빌드**: Gradle 8.5 Kotlin DSL\n- **DB**: MySQL + Spring Data JPA + QueryDSL\n- **캐시/락**: Redis + Redisson (분산락)\n- **외부 API**: Toss Payments (OpenFeign), AWS S3/SES, NCP AlimTalk, Slack API\n- **인증**: Spring Security + JWT + Kakao OAuth\n- **문서화**: SpringDoc OpenAPI (Swagger)\n- **배치**: Spring Batch\n- **실시간**: Spring WebSocket (STOMP)\n- **코드 품질**: SonarQube, Spotless (google-java-format), Jacoco\n\n### 멀티모듈 구조 (의존성 순서)\n\n```\nDuDoong-Backend/\n├── DuDoong-Common/          # 최하위: JWT, 어노테이션, 예외처리, DTO, 유틸\n├── DuDoong-Infrastructure/  # Common 의존: Redis, S3, SES, Feign, Slack, NCP\n├── DuDoong-Domain/          # Common+Infra 의존: JPA 엔티티, 도메인 서비스, AOP\n├── DuDoong-Api/             # 모든 모듈 의존: REST API, Security, Swagger\n├── DuDoong-Socket/          # 모든 모듈 의존: WebSocket STOMP 서버\n└── DuDoong-Batch/           # 모든 모듈 의존: Spring Batch 정산/알림/엑셀\n```\n\n### 도메인 영역 (DuDoong-Domain/domains/)\n\n| 도메인 | 설명 |\n|--------|------|\n| `user` | 사용자 (Kakao OAuth 로그인) |\n| `host` | 이벤트 주최자 |\n| `event` | 이벤트 |\n| `ticket_item` | 티켓 종류 |\n| `order` | 주문/결제 (Toss Payments) |\n| `cart` | 장바구니 |\n| `coupon` | 쿠폰 |\n| `issuedTicket` | 발급 티켓 (QR 입장 검증) |\n| `settlement` | 정산 |\n| `comment` | 댓글 |\n\n### API 컨트롤러 목록\n\n- `AuthController` - 카카오 OAuth 인증\n- `UserController` - 유저 관리\n- `HostController` - 호스트 관리\n- `EventController` - 이벤트 CRUD\n- `TicketItemController` / `TicketOptionController` - 티켓 종류\n- `OrderController` / `OrderAdminController` - 주문/결제\n- `CartController` - 장바구니\n- `CouponController` - 쿠폰\n- `IssuedTicketController` / `AdminIssuedTicketController` - 발급 티켓\n- `ImageController` - S3 Pre-signed URL 이미지 업로드\n- `AdminStatisticController` - 통계\n- `CommentController` - 댓글\n\n### GitHub 저장소\n\n- **Remote**: https://github.com/Gosrock/DuDoong-Backend.git\n- **기본 브랜치**: `dev`\n\n---\n\n## 🚀 Kotlin 마이그레이션 진행 상황\n\n### 마이그레이션 전략: Bottom-up 방식\n\n```\nPhase 0 → Phase 1 → Phase 2 → Phase 3 → Phase 4\n(Gradle)  (Common)  (Infra)   (Domain)   (Api)\n                                    ↘  Phase 5 (Socket)\n                                    ↘  Phase 6 (Batch)\n```\n\n### 이슈 목록\n\n| Phase | 이슈 | 내용 | 상태 |\n|-------|------|------|------|\n| Master | [#582](https://github.com/Gosrock/DuDoong-Backend/issues/582) | 전체 트래킹 | 🔵 진행예정 |\n| Phase 0 | [#583](https://github.com/Gosrock/DuDoong-Backend/issues/583) | Gradle Kotlin DSL 전환 | ⬜ 대기 |\n| Phase 1-1 | [#584](https://github.com/Gosrock/DuDoong-Backend/issues/584) | Common: 어노테이션 & 유틸 | ⬜ 대기 |\n| Phase 1-2 | [#585](https://github.com/Gosrock/DuDoong-Backend/issues/585) | Common: 예외처리 & DTO | ⬜ 대기 |\n| Phase 1-3 | [#586](https://github.com/Gosrock/DuDoong-Backend/issues/586) | Common: JWT & Properties | ⬜ 대기 |\n| Phase 2-1 | [#587](https://github.com/Gosrock/DuDoong-Backend/issues/587) | Infra: Redis & Redisson | ⬜ 대기 |\n| Phase 2-2 | [#588](https://github.com/Gosrock/DuDoong-Backend/issues/588) | Infra: AWS S3 & SES | ⬜ 대기 |\n| Phase 2-3 | [#589](https://github.com/Gosrock/DuDoong-Backend/issues/589) | Infra: Toss Payments Feign | ⬜ 대기 |\n| Phase 2-4 | [#590](https://github.com/Gosrock/DuDoong-Backend/issues/590) | Infra: Slack & AlimTalk | ⬜ 대기 |\n| Phase 3-1 | [#591](https://github.com/Gosrock/DuDoong-Backend/issues/591) | Domain: AOP & 도메인 이벤트 | ⬜ 대기 |\n| Phase 3-2 | [#592](https://github.com/Gosrock/DuDoong-Backend/issues/592) | Domain: User & Host | ⬜ 대기 |\n| Phase 3-3 | [#593](https://github.com/Gosrock/DuDoong-Backend/issues/593) | Domain: Event & TicketItem | ⬜ 대기 |\n| Phase 3-4 | [#594](https://github.com/Gosrock/DuDoong-Backend/issues/594) | Domain: Order & Cart | ⬜ 대기 |\n| Phase 3-5 | [#595](https://github.com/Gosrock/DuDoong-Backend/issues/595) | Domain: IssuedTicket & Coupon | ⬜ 대기 |\n| Phase 3-6 | [#596](https://github.com/Gosrock/DuDoong-Backend/issues/596) | Domain: Settlement & Comment | ⬜ 대기 |\n| Phase 4-1 | [#597](https://github.com/Gosrock/DuDoong-Backend/issues/597) | Api: Spring Security & Auth | ⬜ 대기 |\n| Phase 4-2 | [#598](https://github.com/Gosrock/DuDoong-Backend/issues/598) | Api: User, Host, Event | ⬜ 대기 |\n| Phase 4-3 | [#599](https://github.com/Gosrock/DuDoong-Backend/issues/599) | Api: Order, Cart, Coupon | ⬜ 대기 |\n| Phase 4-4 | [#600](https://github.com/Gosrock/DuDoong-Backend/issues/600) | Api: IssuedTicket, TicketItem | ⬜ 대기 |\n| Phase 4-5 | [#601](https://github.com/Gosrock/DuDoong-Backend/issues/601) | Api: Image, Statistic, 공통 | ⬜ 대기 |\n| Phase 5 | [#602](https://github.com/Gosrock/DuDoong-Backend/issues/602) | Socket: WebSocket 서버 | ⬜ 대기 |\n| Phase 6-1 | [#603](https://github.com/Gosrock/DuDoong-Backend/issues/603) | Batch: 정산 Job | ⬜ 대기 |\n| Phase 6-2 | [#604](https://github.com/Gosrock/DuDoong-Backend/issues/604) | Batch: 만료/엑셀/Slack 통계 | ⬜ 대기 |\n\n---\n\n## 🔐 인증 및 권한 체계\n\n### 인증 방식\n- **로그인**: 카카오 OAuth (`/api/v1/auth/oauth/kakao`) 또는 로컬 개발용 (`/api/v1/auth/oauth/local/login`, dev 전용)\n- **토큰 전달**: `accessToken` 쿠키 (기본) 또는 `Authorization: Bearer` 헤더\n- **토큰 갱신**: `POST /api/v1/auth/token/refresh`\n\n### 역할 (AccountRole)\n| 역할 | 설명 |\n|------|------|\n| `USER` | 일반 유저 |\n| `ADMIN` | 어드민 (내부 관리자) |\n| `SUPER_ADMIN` | 최고 관리자 |\n\n- **MANAGER 역할은 삭제됨** (호스트 멤버십의 HostRole과 혼동 방지)\n- Role hierarchy: `SUPER_ADMIN > ADMIN > USER`\n\n### API 접근 제어 (SecurityConfig)\n| 경로 | 접근 권한 |\n|------|----------|\n| `/api/v1/auth/oauth/**` | permitAll |\n| `/api/v1/events/{id}` (GET) | permitAll |\n| `/api/v1/events/search` (GET) | permitAll |\n| `/internal-api/**` | **ADMIN, SUPER_ADMIN만** |\n| 나머지 `/api/**` | USER 이상 (인증 필수) |\n\n### 호스트 권한 (HostRole AOP)\n- `@HostRolesAllowed` AOP로 호스트 멤버십 기반 권한 체크\n- **userId를 메서드 파라미터로 명시적 전달** (SecurityContext 미사용)\n- `SUPER_ADMIN`은 호스트 멤버가 아니어도 모든 이벤트/호스트 접근 가능 (바이패스)\n- HostQualification: `MASTER` > `MANAGER` > `GUEST`\n\n### Admin API (internal-api)\n- SecurityConfig 레벨: ADMIN/SUPER_ADMIN role 체크\n- UseCase 레벨: `AdminAuthValidator`로 이중 권한 체크\n  - 읽기: `validateAdminOrAbove` (ADMIN+)\n  - 쓰기: `validateAdminOrAbove` (ADMIN+)\n  - 역할 변경: `validateSuperAdmin` (SUPER_ADMIN만)\n- 어드민 전용 로그인 엔드포인트 없음 — 일반 로그인 쿠키 사용\n\n### 어드민 토큰 관련 (레거시)\n- `X-Admin-Token` 헤더, `aud:admin` JWT claim — **사용하지 않음**\n- `AdminLoginUseCase`, `AdminLocalDevLoginUseCase` — **삭제됨**\n- 어드민 접근은 쿠키 기반 + DB role 체크로 통일\n\n---\n\n## 🧪 로컬 개발 & E2E 테스트\n\n### 서버 기동 (로컬 MySQL 사용)\n```bash\n# docker-compose 먼저 (MySQL + Redis)\ndocker compose up -d\n\n# local 프로필로 기동 — 반드시 이 방식으로!\n./gradlew :DuDoong-Api:bootRun --args='--spring.profiles.active=local,infrastructure,domain,domain-local,common,common-local'\n```\n\n**주의**: `local` 프로필 없이 기동하면 **H2 인메모리 DB**를 사용하게 되어 MySQL과 불일치 발생.\n- `spring.profiles.group.local` = `infrastructure, domain-local, common-local`\n- `domain-local` 프로필이 `jdbc:mysql://127.0.0.1:13306/dudoong` 설정\n\n### E2E 테스트 (Python pytest)\n```bash\ncd e2e-tests\npytest -v                          # 전체 실행\npytest test_33* test_34* -v       # 권한 테스트만\n```\n\n### 디버깅 팁\n- API 안 되면 **프로필 먼저 확인** (`profiles are active` 로그)\n- DB 연결 확인: 로그에서 `jdbc:mysql` vs `jdbc:h2:mem` 확인\n- 403 나오면: DB에서 `account_role` 확인 + `hasAnyRole` 매칭 확인\n- 한번에 안 되면 **curl로 한 단계씩 확인** (로그인 → DB 확인 → role 변경 → API 호출)\n\n---\n\n## 🔑 Kotlin 마이그레이션 핵심 원칙\n\n1. **Lombok 제거**: `@Data` → `data class`, `@Builder` → named params + `copy()`, `@RequiredArgsConstructor` → 주생성자\n2. **Null Safety**: `@Nullable`/`@NonNull` → Kotlin `?` / non-null 타입\n3. **KAPT**: QueryDSL Q클래스 생성을 APT → KAPT로 전환\n4. **JPA 엔티티**: `open class` 필수 (`allOpen` plugin), 기본 생성자 자동생성 (`noArg` plugin)\n5. **Jackson**: `jackson-module-kotlin` 등록 필수\n6. **Bean Validation**: `@field:NotBlank` 접두사 필수 (Kotlin data class)\n7. **Spring Security**: Kotlin lambda DSL 활용\n\n## 📋 PR 규칙\n\n- 브랜치명: `feature/kotlin-migration-phase-{N}` 또는 `feature/kotlin-migration-phase-{N}-{name}`\n- PR 본문: `close #이슈번호` 포함\n- 각 Phase 완료 후 빌드(`./gradlew :{Module}:build`) 확인 필수\n\n## ⚠️ 주요 주의사항\n\n- Java ↔ Kotlin 혼재 허용 (마이그레이션 기간 중)\n- CI (GitHub Actions) 빌드 항상 유지\n- `lombok.config` 파일 → 최종 완료 시 삭제\n- SonarQube 설정 → 마지막에 Kotlin 소스로 업데이트\n"
  },
  {
    "path": "DuDoong-Admin/build.gradle.kts",
    "content": "tasks.bootJar { enabled = false }\ntasks.jar { enabled = true }\n\ndependencies {\n    implementation(\"org.springframework.boot:spring-boot-starter-web\")\n    implementation(\"org.springframework.boot:spring-boot-starter-validation\")\n    implementation(\"org.springframework.boot:spring-boot-starter-security\")\n    implementation(\"org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0\")\n    implementation(project(\":DuDoong-Domain\"))\n    implementation(project(\":DuDoong-Common\"))\n    implementation(\"org.apache.poi:poi:5.2.0\")\n    implementation(\"org.apache.poi:poi-ooxml:5.2.0\")\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/controller/AdminCommentController.kt",
    "content": "package band.gosrock.admin.controller\n\nimport band.gosrock.admin.model.dto.response.AdminCommentResponse\nimport band.gosrock.admin.service.AdminDeleteCommentUseCase\nimport band.gosrock.admin.service.AdminGetCommentsUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.web.PageableDefault\nimport org.springframework.http.HttpStatus\nimport org.springframework.web.bind.annotation.DeleteMapping\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RequestParam\nimport org.springframework.web.bind.annotation.ResponseStatus\nimport org.springframework.web.bind.annotation.RestController\n\n@RestController\n@RequestMapping(\"/internal-api/v1/comments\")\n@SecurityRequirement(name = \"admin-token\")\n@Tag(name = \"Admin\")\nclass AdminCommentController(\n    private val adminGetCommentsUseCase: AdminGetCommentsUseCase,\n    private val adminDeleteCommentUseCase: AdminDeleteCommentUseCase,\n) {\n\n    @Operation(summary = \"댓글 목록을 조회합니다.\")\n    @GetMapping\n    fun getComments(\n        @CurrentUserId userId: Long,\n        @RequestParam(required = false) keyword: String?,\n        @RequestParam(required = false) eventId: Long?,\n        @PageableDefault(size = 20) pageable: Pageable,\n    ): Page<AdminCommentResponse> {\n        return adminGetCommentsUseCase.execute(userId, keyword, eventId, pageable)\n    }\n\n    @Operation(summary = \"댓글을 삭제합니다. (소프트 삭제)\")\n    @DeleteMapping(\"/{commentId}\")\n    @ResponseStatus(HttpStatus.NO_CONTENT)\n    fun deleteComment(@CurrentUserId userId: Long, @PathVariable commentId: Long) {\n        adminDeleteCommentUseCase.execute(userId, commentId)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/controller/AdminDashboardController.kt",
    "content": "package band.gosrock.admin.controller\n\nimport band.gosrock.admin.model.dto.response.DashboardResponse\nimport band.gosrock.admin.service.GetDashboardUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport java.time.LocalDate\nimport org.springframework.format.annotation.DateTimeFormat\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RequestParam\nimport org.springframework.web.bind.annotation.RestController\n\n@RestController\n@RequestMapping(\"/internal-api/v1\")\n@SecurityRequirement(name = \"admin-token\")\n@Tag(name = \"Admin\")\nclass AdminDashboardController(\n    private val getDashboardUseCase: GetDashboardUseCase,\n) {\n\n    @Operation(summary = \"어드민 대시보드 통계를 조회합니다.\")\n    @GetMapping(\"/dashboard\")\n    fun getDashboard(\n        @CurrentUserId userId: Long,\n        @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?,\n        @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?,\n    ): DashboardResponse {\n        return getDashboardUseCase.execute(userId, startDate, endDate)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/controller/AdminEventController.kt",
    "content": "package band.gosrock.admin.controller\n\nimport band.gosrock.admin.model.dto.request.AdminAdjustTicketStockRequest\nimport band.gosrock.admin.model.dto.request.AdminUpdateEventRequest\nimport band.gosrock.admin.model.dto.request.AdminUpdateEventStatusRequest\nimport band.gosrock.admin.model.dto.request.AdminUpdateTicketItemRequest\nimport band.gosrock.admin.model.dto.response.AdminEventResponse\nimport band.gosrock.admin.model.dto.response.AdminIssuedTicketResponse\nimport band.gosrock.admin.model.dto.response.AdminTicketItemResponse\nimport band.gosrock.admin.service.AdminAdjustTicketStockUseCase\nimport band.gosrock.admin.service.AdminDeleteEventUseCase\nimport band.gosrock.admin.service.AdminExcelService\nimport band.gosrock.admin.service.AdminExportIssuedTicketsUseCase\nimport band.gosrock.admin.service.AdminGetEventDetailUseCase\nimport band.gosrock.admin.service.AdminGetEventsUseCase\nimport band.gosrock.admin.service.AdminGetIssuedTicketsUseCase\nimport band.gosrock.admin.service.AdminGetTicketItemsUseCase\nimport band.gosrock.admin.service.AdminUpdateEventStatusUseCase\nimport band.gosrock.admin.service.AdminUpdateEventUseCase\nimport band.gosrock.admin.service.AdminUpdateTicketItemUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.web.PageableDefault\nimport org.springframework.http.HttpHeaders\nimport org.springframework.http.HttpStatus\nimport org.springframework.http.MediaType\nimport org.springframework.http.ResponseEntity\nimport org.springframework.web.bind.annotation.DeleteMapping\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PatchMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestBody\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RequestParam\nimport org.springframework.web.bind.annotation.ResponseStatus\nimport org.springframework.web.bind.annotation.RestController\n\n@RestController\n@RequestMapping(\"/internal-api/v1/events\")\n@SecurityRequirement(name = \"admin-token\")\n@Tag(name = \"Admin\")\nclass AdminEventController(\n    private val adminGetEventsUseCase: AdminGetEventsUseCase,\n    private val adminGetEventDetailUseCase: AdminGetEventDetailUseCase,\n    private val adminDeleteEventUseCase: AdminDeleteEventUseCase,\n    private val adminUpdateEventStatusUseCase: AdminUpdateEventStatusUseCase,\n    private val adminUpdateEventUseCase: AdminUpdateEventUseCase,\n    private val adminGetIssuedTicketsUseCase: AdminGetIssuedTicketsUseCase,\n    private val adminExcelService: AdminExcelService,\n    private val adminExportIssuedTicketsUseCase: AdminExportIssuedTicketsUseCase,\n    private val adminGetTicketItemsUseCase: AdminGetTicketItemsUseCase,\n    private val adminUpdateTicketItemUseCase: AdminUpdateTicketItemUseCase,\n    private val adminAdjustTicketStockUseCase: AdminAdjustTicketStockUseCase,\n) {\n\n    @Operation(summary = \"이벤트 목록을 엑셀로 다운로드합니다.\")\n    @GetMapping(\"/export\")\n    fun exportEvents(\n        @CurrentUserId userId: Long,\n        @RequestParam(required = false) keyword: String?,\n        @RequestParam(required = false) status: String?,\n    ): ResponseEntity<ByteArray> {\n        val events = adminGetEventsUseCase.executeAll(userId, keyword, status)\n        val bytes = adminExcelService.generateEventsExcel(events)\n        return ResponseEntity.ok()\n            .header(HttpHeaders.CONTENT_DISPOSITION, \"attachment; filename=events.xlsx\")\n            .contentType(MediaType.APPLICATION_OCTET_STREAM)\n            .body(bytes)\n    }\n\n    @Operation(summary = \"이벤트 목록을 조회합니다.\")\n    @GetMapping\n    fun getEvents(\n        @CurrentUserId userId: Long,\n        @RequestParam(required = false) keyword: String?,\n        @RequestParam(required = false) status: String?,\n        @PageableDefault(size = 20) pageable: Pageable,\n    ): Page<AdminEventResponse> {\n        return adminGetEventsUseCase.execute(userId, keyword, status, pageable)\n    }\n\n    @Operation(summary = \"이벤트 상세 정보를 조회합니다.\")\n    @GetMapping(\"/{eventId}\")\n    fun getEventDetail(@CurrentUserId userId: Long, @PathVariable eventId: Long): AdminEventResponse {\n        return adminGetEventDetailUseCase.execute(userId, eventId)\n    }\n\n    @Operation(summary = \"이벤트를 소프트 삭제합니다.\")\n    @DeleteMapping(\"/{eventId}\")\n    @ResponseStatus(HttpStatus.NO_CONTENT)\n    fun deleteEvent(@CurrentUserId userId: Long, @PathVariable eventId: Long) {\n        adminDeleteEventUseCase.execute(userId, eventId)\n    }\n\n    @Operation(summary = \"이벤트 상태를 변경합니다. (어드민 전용, 밸리데이션 우회)\")\n    @PatchMapping(\"/{eventId}/status\")\n    @ResponseStatus(HttpStatus.NO_CONTENT)\n    fun updateEventStatus(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @RequestBody request: AdminUpdateEventStatusRequest,\n    ) {\n        adminUpdateEventStatusUseCase.execute(userId, eventId, request)\n    }\n\n    @Operation(summary = \"이벤트 정보를 수정합니다. (어드민 전용, OPEN 상태에서도 수정 가능)\")\n    @PatchMapping(\"/{eventId}\")\n    fun updateEvent(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @RequestBody request: AdminUpdateEventRequest,\n    ): AdminEventResponse {\n        return adminUpdateEventUseCase.execute(userId, eventId, request)\n    }\n\n    @Operation(summary = \"이벤트별 발급 티켓 목록을 조회합니다.\")\n    @GetMapping(\"/{eventId}/issued-tickets\")\n    fun getIssuedTickets(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @PageableDefault(size = 20) pageable: Pageable,\n    ): Page<AdminIssuedTicketResponse> {\n        return adminGetIssuedTicketsUseCase.execute(userId, eventId, pageable)\n    }\n\n    @Operation(summary = \"이벤트별 티켓 종류 목록을 조회합니다.\")\n    @GetMapping(\"/{eventId}/ticket-items\")\n    fun getTicketItems(@CurrentUserId userId: Long, @PathVariable eventId: Long): List<AdminTicketItemResponse> {\n        return adminGetTicketItemsUseCase.execute(userId, eventId)\n    }\n\n    @Operation(summary = \"이벤트별 티켓 종류 목록을 엑셀로 다운로드합니다.\")\n    @GetMapping(\"/{eventId}/ticket-items/export\")\n    fun exportTicketItems(@CurrentUserId userId: Long, @PathVariable eventId: Long): ResponseEntity<ByteArray> {\n        val items = adminGetTicketItemsUseCase.execute(userId, eventId)\n        val bytes = adminExcelService.generateTicketItemsExcel(items)\n        return ResponseEntity.ok()\n            .header(HttpHeaders.CONTENT_DISPOSITION, \"attachment; filename=ticket-items-${eventId}.xlsx\")\n            .contentType(MediaType.APPLICATION_OCTET_STREAM)\n            .body(bytes)\n    }\n\n    @Operation(summary = \"이벤트별 발급 티켓 목록을 엑셀로 다운로드합니다. (옵션 응답 동적 컬럼 포함)\")\n    @GetMapping(\"/{eventId}/issued-tickets/export\")\n    fun exportIssuedTickets(@CurrentUserId userId: Long, @PathVariable eventId: Long): ResponseEntity<ByteArray> {\n        val bytes = adminExportIssuedTicketsUseCase.execute(userId, eventId)\n        return ResponseEntity.ok()\n            .header(HttpHeaders.CONTENT_DISPOSITION, \"attachment; filename=issued-tickets-${eventId}.xlsx\")\n            .contentType(MediaType.APPLICATION_OCTET_STREAM)\n            .body(bytes)\n    }\n\n    @Operation(summary = \"티켓 종류를 수정합니다. (어드민 전용, 이벤트 상태 체크 없이 수정 가능)\")\n    @PatchMapping(\"/{eventId}/ticket-items/{ticketItemId}\")\n    fun updateTicketItem(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @PathVariable ticketItemId: Long,\n        @RequestBody request: AdminUpdateTicketItemRequest,\n    ): AdminTicketItemResponse {\n        return adminUpdateTicketItemUseCase.execute(userId, eventId, ticketItemId, request)\n    }\n\n    @Operation(summary = \"티켓 재고를 조정합니다. (어드민 전용, delta 양수=증가 음수=감소)\")\n    @PostMapping(\"/{eventId}/ticket-items/{ticketItemId}/adjust-stock\")\n    fun adjustTicketStock(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @PathVariable ticketItemId: Long,\n        @RequestBody request: AdminAdjustTicketStockRequest,\n    ): AdminTicketItemResponse {\n        return adminAdjustTicketStockUseCase.execute(userId, ticketItemId, request.delta)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/controller/AdminHostController.kt",
    "content": "package band.gosrock.admin.controller\n\nimport band.gosrock.admin.model.dto.request.AdminAddHostMemberRequest\nimport band.gosrock.admin.model.dto.request.AdminTransferMasterRequest\nimport band.gosrock.admin.model.dto.request.AdminUpdateHostMemberRoleRequest\nimport band.gosrock.admin.model.dto.request.AdminUpdateHostPartnerRequest\nimport band.gosrock.admin.model.dto.request.AdminUpdateHostProfileRequest\nimport band.gosrock.admin.model.dto.response.AdminEventResponse\nimport band.gosrock.admin.model.dto.response.AdminHostDetailResponse\nimport band.gosrock.admin.model.dto.response.AdminHostMemberResponse\nimport band.gosrock.admin.model.dto.response.AdminHostResponse\nimport band.gosrock.admin.service.AdminAddHostMemberUseCase\nimport band.gosrock.admin.service.AdminTransferMasterUseCase\nimport band.gosrock.admin.service.AdminGetHostDetailUseCase\nimport band.gosrock.admin.service.AdminGetHostEventsUseCase\nimport band.gosrock.admin.service.AdminGetHostMembersUseCase\nimport band.gosrock.admin.service.AdminGetHostsUseCase\nimport band.gosrock.admin.service.AdminRemoveHostMemberUseCase\nimport band.gosrock.admin.service.AdminUpdateHostMemberRoleUseCase\nimport band.gosrock.admin.service.AdminUpdateHostPartnerUseCase\nimport band.gosrock.admin.service.AdminUpdateHostProfileUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.web.PageableDefault\nimport org.springframework.http.HttpStatus\nimport org.springframework.web.bind.annotation.DeleteMapping\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PatchMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestBody\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RequestParam\nimport org.springframework.web.bind.annotation.ResponseStatus\nimport org.springframework.web.bind.annotation.RestController\n\n@RestController\n@RequestMapping(\"/internal-api/v1/hosts\")\n@SecurityRequirement(name = \"admin-token\")\n@Tag(name = \"Admin\")\nclass AdminHostController(\n    private val adminGetHostsUseCase: AdminGetHostsUseCase,\n    private val adminGetHostDetailUseCase: AdminGetHostDetailUseCase,\n    private val adminGetHostMembersUseCase: AdminGetHostMembersUseCase,\n    private val adminUpdateHostMemberRoleUseCase: AdminUpdateHostMemberRoleUseCase,\n    private val adminAddHostMemberUseCase: AdminAddHostMemberUseCase,\n    private val adminRemoveHostMemberUseCase: AdminRemoveHostMemberUseCase,\n    private val adminGetHostEventsUseCase: AdminGetHostEventsUseCase,\n    private val adminUpdateHostPartnerUseCase: AdminUpdateHostPartnerUseCase,\n    private val adminUpdateHostProfileUseCase: AdminUpdateHostProfileUseCase,\n    private val adminTransferMasterUseCase: AdminTransferMasterUseCase,\n) {\n\n    @Operation(summary = \"호스트 목록을 조회합니다.\")\n    @GetMapping\n    fun getHosts(\n        @CurrentUserId userId: Long,\n        @RequestParam(required = false) keyword: String?,\n        @PageableDefault(size = 20) pageable: Pageable,\n    ): Page<AdminHostResponse> {\n        return adminGetHostsUseCase.execute(userId, keyword, pageable)\n    }\n\n    @Operation(summary = \"호스트 상세 정보를 조회합니다.\")\n    @GetMapping(\"/{hostId}\")\n    fun getHostDetail(@CurrentUserId userId: Long, @PathVariable hostId: Long): AdminHostDetailResponse {\n        return adminGetHostDetailUseCase.execute(userId, hostId)\n    }\n\n    @Operation(summary = \"호스트 소속 멤버 목록을 조회합니다.\")\n    @GetMapping(\"/{hostId}/members\")\n    fun getHostMembers(@CurrentUserId userId: Long, @PathVariable hostId: Long): List<AdminHostMemberResponse> {\n        return adminGetHostMembersUseCase.execute(userId, hostId)\n    }\n\n    @Operation(summary = \"호스트 멤버 역할을 변경합니다.\")\n    @PatchMapping(\"/{hostId}/members/{targetUserId}/role\")\n    fun updateHostMemberRole(\n        @CurrentUserId userId: Long,\n        @PathVariable hostId: Long,\n        @PathVariable targetUserId: Long,\n        @RequestBody request: AdminUpdateHostMemberRoleRequest,\n    ): AdminHostMemberResponse {\n        return adminUpdateHostMemberRoleUseCase.execute(userId, hostId, targetUserId, request)\n    }\n\n    @Operation(summary = \"호스트에 멤버를 추가합니다.\")\n    @PostMapping(\"/{hostId}/members\")\n    @ResponseStatus(HttpStatus.CREATED)\n    fun addHostMember(\n        @CurrentUserId userId: Long,\n        @PathVariable hostId: Long,\n        @RequestBody request: AdminAddHostMemberRequest,\n    ): AdminHostMemberResponse {\n        return adminAddHostMemberUseCase.execute(userId, hostId, request)\n    }\n\n    @Operation(summary = \"호스트에서 멤버를 제거합니다.\")\n    @DeleteMapping(\"/{hostId}/members/{targetUserId}\")\n    @ResponseStatus(HttpStatus.NO_CONTENT)\n    fun removeHostMember(\n        @CurrentUserId userId: Long,\n        @PathVariable hostId: Long,\n        @PathVariable targetUserId: Long,\n    ) {\n        adminRemoveHostMemberUseCase.execute(userId, hostId, targetUserId)\n    }\n\n    @Operation(summary = \"호스트별 이벤트 목록을 조회합니다.\")\n    @GetMapping(\"/{hostId}/events\")\n    fun getHostEvents(\n        @CurrentUserId userId: Long,\n        @PathVariable hostId: Long,\n        @PageableDefault(size = 20) pageable: Pageable,\n    ): Page<AdminEventResponse> {\n        return adminGetHostEventsUseCase.execute(userId, hostId, pageable)\n    }\n\n    @Operation(summary = \"호스트의 파트너 여부를 변경합니다.\")\n    @PatchMapping(\"/{hostId}/partner\")\n    fun updateHostPartner(\n        @CurrentUserId userId: Long,\n        @PathVariable hostId: Long,\n        @RequestBody request: AdminUpdateHostPartnerRequest,\n    ): AdminHostDetailResponse {\n        return adminUpdateHostPartnerUseCase.execute(userId, hostId, request)\n    }\n\n    @Operation(summary = \"호스트 프로필을 수정합니다.\")\n    @PatchMapping(\"/{hostId}/profile\")\n    fun updateHostProfile(\n        @CurrentUserId userId: Long,\n        @PathVariable hostId: Long,\n        @RequestBody request: AdminUpdateHostProfileRequest,\n    ): AdminHostDetailResponse {\n        return adminUpdateHostProfileUseCase.execute(userId, hostId, request)\n    }\n\n    @Operation(summary = \"호스트 마스터 권한을 강제 양도합니다. (어드민)\")\n    @PostMapping(\"/{hostId}/transfer-master\")\n    fun transferMaster(\n        @CurrentUserId userId: Long,\n        @PathVariable hostId: Long,\n        @RequestBody request: AdminTransferMasterRequest,\n    ): AdminHostDetailResponse {\n        return adminTransferMasterUseCase.execute(userId, hostId, request)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/controller/AdminOrderController.kt",
    "content": "package band.gosrock.admin.controller\n\nimport band.gosrock.admin.model.dto.request.AdminCancelOrderRequest\nimport band.gosrock.admin.model.dto.request.AdminRefundStatusRequest\nimport band.gosrock.admin.model.dto.response.AdminOrderResponse\nimport band.gosrock.admin.service.AdminCancelOrderUseCase\nimport band.gosrock.admin.service.AdminExcelService\nimport band.gosrock.admin.service.AdminGetOrderDetailUseCase\nimport band.gosrock.admin.service.AdminGetOrdersUseCase\nimport band.gosrock.admin.service.AdminUpdateRefundStatusUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport jakarta.validation.Valid\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.web.PageableDefault\nimport org.springframework.http.HttpHeaders\nimport org.springframework.http.MediaType\nimport org.springframework.http.ResponseEntity\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PatchMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestBody\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RequestParam\nimport org.springframework.web.bind.annotation.RestController\n\n@RestController\n@RequestMapping(\"/internal-api/v1/orders\")\n@SecurityRequirement(name = \"admin-token\")\n@Tag(name = \"Admin\")\nclass AdminOrderController(\n    private val adminGetOrdersUseCase: AdminGetOrdersUseCase,\n    private val adminGetOrderDetailUseCase: AdminGetOrderDetailUseCase,\n    private val adminCancelOrderUseCase: AdminCancelOrderUseCase,\n    private val adminUpdateRefundStatusUseCase: AdminUpdateRefundStatusUseCase,\n    private val adminExcelService: AdminExcelService,\n) {\n\n    @Operation(summary = \"주문 목록을 엑셀로 다운로드합니다.\")\n    @GetMapping(\"/export\")\n    fun exportOrders(\n        @CurrentUserId userId: Long,\n        @RequestParam(required = false) keyword: String?,\n        @RequestParam(required = false) status: OrderStatus?,\n        @RequestParam(required = false) eventId: Long?,\n    ): ResponseEntity<ByteArray> {\n        val orders = adminGetOrdersUseCase.executeAll(userId, keyword, status, eventId)\n        val bytes = adminExcelService.generateOrdersExcel(orders)\n        return ResponseEntity.ok()\n            .header(HttpHeaders.CONTENT_DISPOSITION, \"attachment; filename=orders.xlsx\")\n            .contentType(MediaType.APPLICATION_OCTET_STREAM)\n            .body(bytes)\n    }\n\n    @Operation(summary = \"주문 목록을 조회합니다.\")\n    @GetMapping\n    fun getOrders(\n        @CurrentUserId userId: Long,\n        @RequestParam(required = false) keyword: String?,\n        @RequestParam(required = false) status: OrderStatus?,\n        @RequestParam(required = false) eventId: Long?,\n        @PageableDefault(size = 20) pageable: Pageable,\n    ): Page<AdminOrderResponse> {\n        return adminGetOrdersUseCase.execute(userId, keyword, status, eventId, pageable)\n    }\n\n    @Operation(summary = \"주문 상세 정보를 조회합니다.\")\n    @GetMapping(\"/{orderUuid}\")\n    fun getOrderDetail(@CurrentUserId userId: Long, @PathVariable orderUuid: String): AdminOrderResponse {\n        return adminGetOrderDetailUseCase.execute(userId, orderUuid)\n    }\n\n    @Operation(summary = \"주문을 취소합니다.\")\n    @PostMapping(\"/{orderUuid}/cancel\")\n    fun cancelOrder(\n        @CurrentUserId userId: Long,\n        @PathVariable orderUuid: String,\n        @RequestBody(required = false) request: AdminCancelOrderRequest?,\n    ): AdminOrderResponse {\n        return adminCancelOrderUseCase.execute(userId, orderUuid, request?.reason)\n    }\n\n    @Operation(summary = \"주문의 환불 상태를 변경합니다. (REFUND_COMPLETED)\")\n    @PatchMapping(\"/{orderUuid}/refund-status\")\n    fun updateRefundStatus(\n        @CurrentUserId userId: Long,\n        @PathVariable orderUuid: String,\n        @RequestBody @Valid request: AdminRefundStatusRequest,\n    ): AdminOrderResponse {\n        return adminUpdateRefundStatusUseCase.execute(userId, orderUuid, request)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/controller/AdminRefundController.kt",
    "content": "package band.gosrock.admin.controller\n\nimport band.gosrock.admin.model.dto.response.AdminRefundResponse\nimport band.gosrock.admin.service.AdminCompleteRefundUseCase\nimport band.gosrock.admin.service.AdminGetRefundDetailUseCase\nimport band.gosrock.admin.service.AdminGetRefundsUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport band.gosrock.domain.domains.order.domain.RefundStatus\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.web.PageableDefault\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PatchMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RequestParam\nimport org.springframework.web.bind.annotation.RestController\n\n@RestController\n@RequestMapping(\"/internal-api/v1/refunds\")\n@SecurityRequirement(name = \"admin-token\")\n@Tag(name = \"Admin\")\nclass AdminRefundController(\n    private val adminGetRefundsUseCase: AdminGetRefundsUseCase,\n    private val adminGetRefundDetailUseCase: AdminGetRefundDetailUseCase,\n    private val adminCompleteRefundUseCase: AdminCompleteRefundUseCase,\n) {\n\n    @Operation(summary = \"전체 환불 목록을 조회합니다.\")\n    @GetMapping\n    fun getRefunds(\n        @CurrentUserId userId: Long,\n        @RequestParam(required = false) refundStatus: RefundStatus?,\n        @RequestParam(required = false) eventId: Long?,\n        @RequestParam(required = false) keyword: String?,\n        @PageableDefault(size = 20) pageable: Pageable,\n    ): Page<AdminRefundResponse> =\n        adminGetRefundsUseCase.execute(userId, refundStatus, eventId, keyword, pageable)\n\n    @Operation(summary = \"환불 상세 정보를 조회합니다.\")\n    @GetMapping(\"/{orderUuid}\")\n    fun getRefundDetail(\n        @CurrentUserId userId: Long,\n        @PathVariable orderUuid: String,\n    ): AdminRefundResponse =\n        adminGetRefundDetailUseCase.execute(userId, orderUuid)\n\n    @Operation(summary = \"환불을 확인 처리합니다. (REFUND_COMPLETED)\")\n    @PatchMapping(\"/{orderUuid}/complete\")\n    fun completeRefund(\n        @CurrentUserId userId: Long,\n        @PathVariable orderUuid: String,\n    ): AdminRefundResponse =\n        adminCompleteRefundUseCase.execute(userId, orderUuid)\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/controller/AdminUserController.kt",
    "content": "package band.gosrock.admin.controller\n\nimport band.gosrock.admin.model.dto.request.AdminChangeNameRequest\nimport band.gosrock.admin.model.dto.request.AdminUpdateUserRoleRequest\nimport band.gosrock.admin.model.dto.request.AdminUpdateUserStatusRequest\nimport band.gosrock.admin.model.dto.response.AdminUserDetailResponse\nimport band.gosrock.admin.model.dto.response.AdminUserResponse\nimport band.gosrock.admin.service.AdminChangeNameUseCase\nimport band.gosrock.admin.service.AdminExcelService\nimport band.gosrock.admin.service.AdminGetUserDetailUseCase\nimport band.gosrock.admin.service.AdminGetUsersUseCase\nimport band.gosrock.admin.service.AdminUpdateUserRoleUseCase\nimport band.gosrock.admin.service.AdminUpdateUserStatusUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport jakarta.validation.Valid\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.web.PageableDefault\nimport org.springframework.http.HttpHeaders\nimport org.springframework.http.MediaType\nimport org.springframework.http.ResponseEntity\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PatchMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.RequestBody\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RequestParam\nimport org.springframework.web.bind.annotation.RestController\n\n@RestController\n@RequestMapping(\"/internal-api/v1/users\")\n@SecurityRequirement(name = \"admin-token\")\n@Tag(name = \"Admin\")\nclass AdminUserController(\n    private val adminGetUsersUseCase: AdminGetUsersUseCase,\n    private val adminGetUserDetailUseCase: AdminGetUserDetailUseCase,\n    private val adminUpdateUserRoleUseCase: AdminUpdateUserRoleUseCase,\n    private val adminUpdateUserStatusUseCase: AdminUpdateUserStatusUseCase,\n    private val adminChangeNameUseCase: AdminChangeNameUseCase,\n    private val adminExcelService: AdminExcelService,\n) {\n\n    @Operation(summary = \"유저 목록을 엑셀로 다운로드합니다.\")\n    @GetMapping(\"/export\")\n    fun exportUsers(\n        @CurrentUserId currentUserId: Long,\n        @RequestParam(required = false) keyword: String?,\n    ): ResponseEntity<ByteArray> {\n        val users = adminGetUsersUseCase.executeAll(currentUserId, keyword)\n        val bytes = adminExcelService.generateUsersExcel(users)\n        return ResponseEntity.ok()\n            .header(HttpHeaders.CONTENT_DISPOSITION, \"attachment; filename=users.xlsx\")\n            .contentType(MediaType.APPLICATION_OCTET_STREAM)\n            .body(bytes)\n    }\n\n    @Operation(summary = \"유저 목록을 조회합니다.\")\n    @GetMapping\n    fun getUsers(\n        @CurrentUserId currentUserId: Long,\n        @RequestParam(required = false) keyword: String?,\n        @PageableDefault(size = 20) pageable: Pageable,\n    ): Page<AdminUserResponse> {\n        return adminGetUsersUseCase.execute(currentUserId, keyword, pageable)\n    }\n\n    @Operation(summary = \"유저 상세 정보를 조회합니다.\")\n    @GetMapping(\"/{userId}\")\n    fun getUserDetail(\n        @CurrentUserId currentUserId: Long,\n        @PathVariable userId: Long,\n    ): AdminUserDetailResponse {\n        return adminGetUserDetailUseCase.execute(currentUserId, userId)\n    }\n\n    @Operation(summary = \"유저 역할을 변경합니다. (SUPER_ADMIN 전용)\")\n    @PatchMapping(\"/{userId}/role\")\n    fun updateUserRole(\n        @CurrentUserId currentUserId: Long,\n        @PathVariable userId: Long,\n        @RequestBody request: AdminUpdateUserRoleRequest,\n    ): AdminUserResponse {\n        return adminUpdateUserRoleUseCase.execute(currentUserId, userId, request)\n    }\n\n    @Operation(summary = \"유저 상태를 변경합니다.\")\n    @PatchMapping(\"/{userId}/status\")\n    fun updateUserStatus(\n        @CurrentUserId currentUserId: Long,\n        @PathVariable userId: Long,\n        @RequestBody request: AdminUpdateUserStatusRequest,\n    ): AdminUserResponse {\n        return adminUpdateUserStatusUseCase.execute(currentUserId, userId, request)\n    }\n\n    @Operation(summary = \"유저 이름 변경\")\n    @PatchMapping(\"/{userId}/name\")\n    fun changeUserName(\n        @CurrentUserId adminUserId: Long,\n        @PathVariable userId: Long,\n        @Valid @RequestBody request: AdminChangeNameRequest,\n    ) {\n        adminChangeNameUseCase.execute(adminUserId, userId, request)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/exception/AdminErrorCode.kt",
    "content": "package band.gosrock.admin.exception\n\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.consts.DuDoongStatic.FORBIDDEN\nimport band.gosrock.common.dto.ErrorReason\nimport band.gosrock.common.exception.BaseErrorCode\nimport java.lang.reflect.Field\n\nenum class AdminErrorCode(\n    private val status: Int,\n    private val code: String,\n    private val reason: String,\n) : BaseErrorCode {\n    @ExplainError(\"어드민 권한이 없는 유저가 어드민 기능에 접근하려는 경우\")\n    ADMIN_FORBIDDEN(FORBIDDEN, \"ADMIN_403_1\", \"어드민 권한이 필요합니다. MANAGER 이상의 역할이 필요합니다.\"),\n\n    @ExplainError(\"SUPER_ADMIN 전용 기능에 일반 관리자가 접근하려는 경우\")\n    ADMIN_SUPER_ADMIN_REQUIRED(FORBIDDEN, \"ADMIN_403_2\", \"SUPER_ADMIN 권한이 필요합니다.\");\n\n    override fun getErrorReason(): ErrorReason =\n        ErrorReason(status = status, code = code, reason = reason)\n\n    override fun getExplainError(): String {\n        val field: Field = this.javaClass.getField(this.name)\n        val annotation = field.getAnnotation(ExplainError::class.java)\n        return annotation?.value ?: reason\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/exception/AdminForbiddenException.kt",
    "content": "package band.gosrock.admin.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass AdminForbiddenException private constructor() : DuDoongCodeException(AdminErrorCode.ADMIN_FORBIDDEN) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = AdminForbiddenException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminAddHostMemberRequest.kt",
    "content": "package band.gosrock.admin.model.dto.request\n\nimport band.gosrock.domain.domains.host.domain.HostRole\n\ndata class AdminAddHostMemberRequest(\n    val userId: Long,\n    val role: HostRole,\n)\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminAdjustTicketStockRequest.kt",
    "content": "package band.gosrock.admin.model.dto.request\n\ndata class AdminAdjustTicketStockRequest(\n    val delta: Long, // 양수 = 증가, 음수 = 감소\n)\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminCancelOrderRequest.kt",
    "content": "package band.gosrock.admin.model.dto.request\n\ndata class AdminCancelOrderRequest(\n    val reason: String? = null,\n)\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminChangeNameRequest.kt",
    "content": "package band.gosrock.admin.model.dto.request\n\nimport jakarta.validation.constraints.NotBlank\nimport jakarta.validation.constraints.Size\n\ndata class AdminChangeNameRequest(\n    @field:NotBlank(message = \"이름을 입력해주세요.\")\n    @field:Size(min = 2, max = 7, message = \"이름은 2~7자여야 합니다.\")\n    val name: String,\n)\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminRefundStatusRequest.kt",
    "content": "package band.gosrock.admin.model.dto.request\n\nimport jakarta.validation.constraints.NotNull\n\ndata class AdminRefundStatusRequest(\n    @field:NotNull\n    val refundStatus: String,\n    val reason: String? = null,\n)\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminTransferMasterRequest.kt",
    "content": "package band.gosrock.admin.model.dto.request\n\nimport jakarta.validation.constraints.NotNull\n\n/** 어드민 호스트 마스터 강제 양도 요청 DTO */\ndata class AdminTransferMasterRequest(\n    @field:NotNull(message = \"양도 대상 유저 아이디를 입력해주세요\")\n    val newMasterUserId: Long,\n)\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminUpdateEventRequest.kt",
    "content": "package band.gosrock.admin.model.dto.request\n\nimport java.time.LocalDateTime\n\ndata class AdminUpdateEventRequest(\n    val name: String? = null,\n    val startAt: LocalDateTime? = null,\n    val runTime: Int? = null,\n    val content: String? = null,\n    val placeName: String? = null,\n    val placeAddress: String? = null,\n)\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminUpdateEventStatusRequest.kt",
    "content": "package band.gosrock.admin.model.dto.request\n\nimport band.gosrock.domain.domains.event.domain.EventStatus\n\ndata class AdminUpdateEventStatusRequest(\n    val status: EventStatus,\n)\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminUpdateHostMemberRoleRequest.kt",
    "content": "package band.gosrock.admin.model.dto.request\n\nimport band.gosrock.domain.domains.host.domain.HostRole\n\ndata class AdminUpdateHostMemberRoleRequest(\n    val role: HostRole,\n)\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminUpdateHostPartnerRequest.kt",
    "content": "package band.gosrock.admin.model.dto.request\n\ndata class AdminUpdateHostPartnerRequest(\n    val partner: Boolean,\n)\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminUpdateHostProfileRequest.kt",
    "content": "package band.gosrock.admin.model.dto.request\n\ndata class AdminUpdateHostProfileRequest(\n    val name: String?,\n    val introduce: String?,\n    val contactEmail: String?,\n    val contactNumber: String?,\n)\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminUpdateTicketItemRequest.kt",
    "content": "package band.gosrock.admin.model.dto.request\n\nimport java.math.BigDecimal\n\ndata class AdminUpdateTicketItemRequest(\n    val name: String? = null,\n    val description: String? = null,\n    val price: BigDecimal? = null,\n    val quantity: Long? = null,\n    val purchaseLimit: Long? = null,\n)\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminUpdateUserRoleRequest.kt",
    "content": "package band.gosrock.admin.model.dto.request\n\nimport band.gosrock.domain.domains.user.domain.AccountRole\n\ndata class AdminUpdateUserRoleRequest(\n    val role: AccountRole,\n)\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/request/AdminUpdateUserStatusRequest.kt",
    "content": "package band.gosrock.admin.model.dto.request\n\nimport band.gosrock.domain.domains.user.domain.AccountState\n\ndata class AdminUpdateUserStatusRequest(\n    val status: AccountState,\n)\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminCommentResponse.kt",
    "content": "package band.gosrock.admin.model.dto.response\n\nimport band.gosrock.domain.domains.comment.domain.Comment\nimport band.gosrock.domain.domains.comment.domain.CommentStatus\nimport java.time.LocalDateTime\n\ndata class AdminCommentResponse(\n    val id: Long,\n    val userName: String?,\n    val eventName: String?,\n    val content: String?,\n    val commentStatus: CommentStatus?,\n    val createdAt: LocalDateTime?,\n    val userId: Long?,\n    val eventId: Long?,\n) {\n    companion object {\n        fun of(comment: Comment, eventName: String?): AdminCommentResponse =\n            AdminCommentResponse(\n                id = comment.id!!,\n                userName = comment.nickName,\n                eventName = eventName,\n                content = comment.content,\n                commentStatus = comment.commentStatus,\n                createdAt = comment.createdAt,\n                userId = comment.user?.id,\n                eventId = comment.eventId,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminEventResponse.kt",
    "content": "package band.gosrock.admin.model.dto.response\n\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.event.domain.EventStatus\nimport java.time.LocalDateTime\n\ndata class AdminEventResponse(\n    val id: Long,\n    val name: String?,\n    val hostName: String?,\n    val status: EventStatus,\n    val startAt: LocalDateTime?,\n    val runTime: Long?,\n    val createdAt: LocalDateTime?,\n    val ticketItemCount: Int = 0,\n    val issuedTicketCount: Int = 0,\n    val totalOrderCount: Int = 0,\n    val content: String? = null,\n    val placeName: String? = null,\n    val placeAddress: String? = null,\n    val hostId: Long? = null,\n    val posterImageKey: String? = null,\n    val latitude: Double? = null,\n    val longitude: Double? = null,\n) {\n    companion object {\n        fun of(event: Event, hostName: String?): AdminEventResponse =\n            AdminEventResponse(\n                id = event.id!!,\n                name = event.eventBasic?.name,\n                hostName = hostName,\n                status = event.status,\n                startAt = event.eventBasic?.startAt,\n                runTime = event.eventBasic?.runTime,\n                createdAt = event.createdAt,\n                hostId = event.hostId,\n                posterImageKey = event.eventDetail?.posterImage?.imageKey,\n                latitude = event.eventPlace?.latitude,\n                longitude = event.eventPlace?.longitude,\n            )\n\n        fun ofDetail(\n            event: Event,\n            hostName: String?,\n            ticketItemCount: Int,\n            issuedTicketCount: Int,\n            totalOrderCount: Int,\n        ): AdminEventResponse =\n            AdminEventResponse(\n                id = event.id!!,\n                name = event.eventBasic?.name,\n                hostName = hostName,\n                status = event.status,\n                startAt = event.eventBasic?.startAt,\n                runTime = event.eventBasic?.runTime,\n                createdAt = event.createdAt,\n                ticketItemCount = ticketItemCount,\n                issuedTicketCount = issuedTicketCount,\n                totalOrderCount = totalOrderCount,\n                content = event.eventDetail?.content,\n                placeName = event.eventPlace?.placeName,\n                placeAddress = event.eventPlace?.placeAddress,\n                hostId = event.hostId,\n                posterImageKey = event.eventDetail?.posterImage?.imageKey,\n                latitude = event.eventPlace?.latitude,\n                longitude = event.eventPlace?.longitude,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminHostDetailResponse.kt",
    "content": "package band.gosrock.admin.model.dto.response\n\nimport band.gosrock.domain.domains.host.domain.Host\nimport java.time.LocalDateTime\n\ndata class AdminHostDetailResponse(\n    val id: Long,\n    val name: String?,\n    val introduce: String?,\n    val contactEmail: String?,\n    val contactNumber: String?,\n    val profileImage: String?,\n    val partner: Boolean,\n    val masterUserId: Long?,\n    val createdAt: LocalDateTime?,\n    val memberCount: Int,\n    val slackUrl: String?,\n) {\n    companion object {\n        fun from(host: Host): AdminHostDetailResponse =\n            AdminHostDetailResponse(\n                id = host.id!!,\n                name = host.profile?.name,\n                introduce = host.profile?.introduce,\n                contactEmail = host.profile?.contactEmail,\n                contactNumber = host.profile?.contactNumber,\n                profileImage = host.profile?.profileImage?.imageKey,\n                partner = host.partner,\n                masterUserId = host.masterUserId,\n                createdAt = host.createdAt,\n                memberCount = host.hostUsers.size,\n                slackUrl = host.slackUrl,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminHostMemberResponse.kt",
    "content": "package band.gosrock.admin.model.dto.response\n\nimport band.gosrock.domain.domains.host.domain.HostRole\nimport band.gosrock.domain.domains.host.domain.HostUser\nimport java.time.LocalDateTime\n\ndata class AdminHostMemberResponse(\n    val userId: Long?,\n    val userName: String?,\n    val role: HostRole,\n    val active: Boolean,\n    val createdAt: LocalDateTime?,\n) {\n    companion object {\n        fun of(hostUser: HostUser, userName: String?): AdminHostMemberResponse =\n            AdminHostMemberResponse(\n                userId = hostUser.userId,\n                userName = userName,\n                role = hostUser.role,\n                active = hostUser.active,\n                createdAt = hostUser.createdAt,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminHostResponse.kt",
    "content": "package band.gosrock.admin.model.dto.response\n\nimport band.gosrock.domain.domains.host.domain.Host\nimport java.time.LocalDateTime\n\ndata class AdminHostResponse(\n    val id: Long,\n    val name: String?,\n    val introduce: String?,\n    val contactEmail: String?,\n    val contactNumber: String?,\n    val profileImage: String?,\n    val partner: Boolean,\n    val masterUserId: Long?,\n    val createdAt: LocalDateTime?,\n) {\n    companion object {\n        fun from(host: Host): AdminHostResponse =\n            AdminHostResponse(\n                id = host.id!!,\n                name = host.profile?.name,\n                introduce = host.profile?.introduce,\n                contactEmail = host.profile?.contactEmail,\n                contactNumber = host.profile?.contactNumber,\n                profileImage = host.profile?.profileImage?.imageKey,\n                partner = host.partner,\n                masterUserId = host.masterUserId,\n                createdAt = host.createdAt,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminIssuedTicketResponse.kt",
    "content": "package band.gosrock.admin.model.dto.response\n\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketStatus\nimport java.time.LocalDateTime\n\ndata class AdminIssuedTicketResponse(\n    val id: Long,\n    val issuedTicketNo: String?,\n    val userName: String?,\n    val ticketName: String?,\n    val orderUuid: String?,\n    val enteredAt: LocalDateTime?,\n    val status: IssuedTicketStatus,\n    val createdAt: LocalDateTime?,\n) {\n    companion object {\n        fun from(issuedTicket: IssuedTicket): AdminIssuedTicketResponse =\n            AdminIssuedTicketResponse(\n                id = issuedTicket.id!!,\n                issuedTicketNo = issuedTicket.issuedTicketNo,\n                userName = issuedTicket.userInfo?.userName,\n                ticketName = issuedTicket.itemInfo?.ticketName,\n                orderUuid = issuedTicket.orderUuid,\n                enteredAt = issuedTicket.enteredAt,\n                status = issuedTicket.issuedTicketStatus,\n                createdAt = issuedTicket.createdAt,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminOrderResponse.kt",
    "content": "package band.gosrock.admin.model.dto.response\n\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport java.time.LocalDateTime\n\ndata class AdminOrderResponse(\n    val orderId: String?,\n    val userName: String?,\n    val eventName: String?,\n    val ticketName: String?,\n    val totalAmount: Long,\n    val orderStatus: OrderStatus,\n    val createdAt: LocalDateTime?,\n    val orderNo: String?,\n    val orderMethod: String?,\n    val userId: Long?,\n    val eventId: Long?,\n    val approvedAt: LocalDateTime?,\n    val withDrawAt: LocalDateTime?,\n    val paymentMethod: String?,\n    val receiptUrl: String?,\n    val supplyAmount: String?,\n    val discountAmount: String?,\n    val couponName: String?,\n    val failReason: String? = null,\n    val cancelReason: String? = null,\n    val refundStatus: String? = null,\n    val refundStatusChangedAt: String? = null,\n) {\n    companion object {\n        fun of(order: Order, userName: String?, eventName: String?): AdminOrderResponse =\n            AdminOrderResponse(\n                orderId = order.uuid,\n                userName = userName,\n                eventName = eventName,\n                ticketName = order.orderName,\n                totalAmount = order.getTotalPaymentPrice().longValue(),\n                orderStatus = order.orderStatus,\n                createdAt = order.createdAt,\n                orderNo = order.orderNo,\n                orderMethod = order.orderMethod?.name,\n                userId = order.userId,\n                eventId = order.eventId,\n                approvedAt = order.approvedAt,\n                withDrawAt = order.withDrawAt,\n                paymentMethod = order.pgPaymentInfo.paymentMethod.name,\n                receiptUrl = order.pgPaymentInfo.receiptUrl,\n                supplyAmount = order.totalPaymentInfo?.supplyAmount?.toString(),\n                discountAmount = order.totalPaymentInfo?.discountAmount?.toString(),\n                couponName = order.orderCouponVo.name,\n                failReason = order.failReason,\n                cancelReason = order.cancelReason,\n                refundStatus = order.refundStatus.name,\n                refundStatusChangedAt = order.refundStatusChangedAt?.toString(),\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminRefundResponse.kt",
    "content": "package band.gosrock.admin.model.dto.response\n\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.RefundStatus\nimport java.time.LocalDateTime\n\ndata class AdminRefundResponse(\n    val orderId: String?,\n    val orderNo: String?,\n    val userName: String?,\n    val eventName: String?,\n    val eventId: Long?,\n    val ticketName: String?,\n    val totalAmount: Long,\n    val cancelReason: String?,\n    val refundStatus: RefundStatus,\n    val refundStatusChangedAt: LocalDateTime?,\n    val withDrawAt: LocalDateTime?,\n    val createdAt: LocalDateTime?,\n    val userId: Long?,\n) {\n    companion object {\n        fun of(order: Order, userName: String?, eventName: String?): AdminRefundResponse =\n            AdminRefundResponse(\n                orderId = order.uuid,\n                orderNo = order.orderNo,\n                userName = userName,\n                eventName = eventName,\n                eventId = order.eventId,\n                ticketName = order.orderName,\n                totalAmount = order.getTotalPaymentPrice().longValue(),\n                cancelReason = order.cancelReason,\n                refundStatus = order.refundStatus,\n                refundStatusChangedAt = order.refundStatusChangedAt,\n                withDrawAt = order.withDrawAt,\n                createdAt = order.createdAt,\n                userId = order.userId,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminTicketItemResponse.kt",
    "content": "package band.gosrock.admin.model.dto.response\n\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItemStatus\nimport band.gosrock.domain.domains.ticket_item.domain.TicketPayType\nimport band.gosrock.domain.domains.ticket_item.domain.TicketType\nimport java.math.BigDecimal\nimport java.time.LocalDateTime\n\ndata class AdminTicketItemResponse(\n    val id: Long,\n    val name: String?,\n    val description: String?,\n    val price: BigDecimal?,\n    val quantity: Long?,\n    val supplyCount: Long?,\n    val purchaseLimit: Long?,\n    val payType: TicketPayType?,\n    val type: TicketType?,\n    val isQuantityPublic: Boolean?,\n    val isSellable: Boolean?,\n    val saleStartAt: LocalDateTime?,\n    val saleEndAt: LocalDateTime?,\n    val ticketItemStatus: TicketItemStatus,\n    val eventId: Long?,\n) {\n    companion object {\n        fun from(ticketItem: TicketItem): AdminTicketItemResponse =\n            AdminTicketItemResponse(\n                id = ticketItem.id!!,\n                name = ticketItem.name,\n                description = ticketItem.description,\n                price = ticketItem.price?.amount,\n                quantity = ticketItem.quantity,\n                supplyCount = ticketItem.supplyCount,\n                purchaseLimit = ticketItem.purchaseLimit,\n                payType = ticketItem.payType,\n                type = ticketItem.type,\n                isQuantityPublic = ticketItem.isQuantityPublic,\n                isSellable = ticketItem.isSellable,\n                saleStartAt = ticketItem.saleStartAt,\n                saleEndAt = ticketItem.saleEndAt,\n                ticketItemStatus = ticketItem.ticketItemStatus,\n                eventId = ticketItem.eventId,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminUserDetailResponse.kt",
    "content": "package band.gosrock.admin.model.dto.response\n\nimport band.gosrock.domain.domains.user.domain.AccountRole\nimport band.gosrock.domain.domains.user.domain.AccountState\nimport band.gosrock.domain.domains.user.domain.OauthProvider\nimport band.gosrock.domain.domains.user.domain.User\nimport java.time.LocalDateTime\n\ndata class AdminUserDetailResponse(\n    val id: Long,\n    val name: String?,\n    val email: String?,\n    val profileImage: String?,\n    val accountRole: AccountRole,\n    val accountState: AccountState,\n    val createdAt: LocalDateTime?,\n    val phoneNumber: String?,\n    val marketingAgree: Boolean,\n    val oauthProvider: OauthProvider?,\n    val lastLoginAt: LocalDateTime?,\n    val receiveMail: Boolean,\n) {\n    companion object {\n        fun from(user: User): AdminUserDetailResponse =\n            AdminUserDetailResponse(\n                id = user.id!!,\n                name = user.profile?.name,\n                email = user.profile?.email,\n                profileImage = user.profile?.profileImage?.imageKey,\n                accountRole = user.accountRole,\n                accountState = user.accountState,\n                createdAt = user.createdAt,\n                phoneNumber = user.profile?.phoneNumberVo?.phoneNumber,\n                marketingAgree = user.marketingAgree,\n                oauthProvider = user.oauthInfo?.provider,\n                lastLoginAt = user.lastLoginAt,\n                receiveMail = user.receiveMail,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/AdminUserResponse.kt",
    "content": "package band.gosrock.admin.model.dto.response\n\nimport band.gosrock.domain.domains.user.domain.AccountRole\nimport band.gosrock.domain.domains.user.domain.AccountState\nimport band.gosrock.domain.domains.user.domain.User\nimport java.time.LocalDateTime\n\ndata class AdminUserResponse(\n    val id: Long,\n    val name: String?,\n    val email: String?,\n    val profileImage: String?,\n    val accountRole: AccountRole,\n    val accountState: AccountState,\n    val createdAt: LocalDateTime?,\n) {\n    companion object {\n        fun from(user: User): AdminUserResponse =\n            AdminUserResponse(\n                id = user.id!!,\n                name = user.profile?.name,\n                email = user.profile?.email,\n                profileImage = user.profile?.profileImage?.imageKey,\n                accountRole = user.accountRole,\n                accountState = user.accountState,\n                createdAt = user.createdAt,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/model/dto/response/DashboardResponse.kt",
    "content": "package band.gosrock.admin.model.dto.response\n\ndata class DashboardResponse(\n    val totalUsers: Long,\n    val todayNewUsers: Long,\n    val todayOrders: Long,\n    val todayRevenue: Long,\n    val activeEvents: Long,\n    val todayRefunds: Long,\n    val recentOrders: List<AdminOrderResponse> = emptyList(),\n    val recentEvents: List<AdminEventResponse> = emptyList(),\n)\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminAddHostMemberUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.request.AdminAddHostMemberRequest\nimport band.gosrock.admin.model.dto.response.AdminHostMemberResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.domain.HostUser\nimport band.gosrock.domain.domains.host.repository.HostRepository\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass AdminAddHostMemberUseCase(\n    private val hostAdaptor: HostAdaptor,\n    private val hostRepository: HostRepository,\n    private val userAdaptor: UserAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    @Transactional\n    fun execute(userId: Long, hostId: Long, request: AdminAddHostMemberRequest): AdminHostMemberResponse {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val host = hostAdaptor.findById(hostId)\n        val hostUser = HostUser(host, request.userId, request.role)\n        host.addHostUsers(setOf(hostUser))\n        hostRepository.save(host)\n\n        val userName = runCatching { userAdaptor.queryUser(request.userId).profile?.name }.getOrNull()\n        val savedHostUser = host.getHostUserByUserId(request.userId)\n        return AdminHostMemberResponse.of(savedHostUser, userName)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminAdjustTicketStockUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminTicketItemResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.common.aop.redissonLock.RedissonLock\nimport band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor\n\n@UseCase\nclass AdminAdjustTicketStockUseCase(\n    private val ticketItemAdaptor: TicketItemAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n    @RedissonLock(LockName = \"티켓관리\", identifier = \"ticketItemId\")\n    fun execute(userId: Long, ticketItemId: Long, delta: Long): AdminTicketItemResponse {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val ticketItem = ticketItemAdaptor.queryTicketItem(ticketItemId)\n        ticketItem.adminAdjustStock(delta)\n        ticketItemAdaptor.save(ticketItem)\n        return AdminTicketItemResponse.from(ticketItem)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminAuthValidator.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.exception.AdminForbiddenException\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.domain.domains.user.domain.AccountRole\nimport band.gosrock.domain.domains.user.domain.User\nimport org.slf4j.LoggerFactory\nimport org.springframework.stereotype.Component\n\n@Component\nclass AdminAuthValidator(\n    private val userAdaptor: UserAdaptor,\n) {\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    fun validateAdminOrAbove(userId: Long): User {\n        val user = userAdaptor.queryUser(userId)\n        if (user.accountRole != AccountRole.ADMIN && user.accountRole != AccountRole.SUPER_ADMIN) {\n            log.info(\"[ADMIN-AUTH] DENIED - userId={}, role={}, required=ADMIN+\", userId, user.accountRole)\n            throw AdminForbiddenException.EXCEPTION\n        }\n        log.info(\"[ADMIN-AUTH] GRANTED - userId={}, role={}\", userId, user.accountRole)\n        return user\n    }\n\n    fun validateSuperAdmin(userId: Long): User {\n        val user = userAdaptor.queryUser(userId)\n        if (user.accountRole != AccountRole.SUPER_ADMIN) {\n            log.info(\"[ADMIN-AUTH] DENIED - userId={}, role={}, required=SUPER_ADMIN\", userId, user.accountRole)\n            throw AdminForbiddenException.EXCEPTION\n        }\n        log.info(\"[ADMIN-AUTH] GRANTED - userId={}, role=SUPER_ADMIN\", userId)\n        return user\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminCancelOrderUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminOrderResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.domain.validator.OrderValidator\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass AdminCancelOrderUseCase(\n    private val orderAdaptor: OrderAdaptor,\n    private val orderValidator: OrderValidator,\n    private val userAdaptor: UserAdaptor,\n    private val eventAdaptor: EventAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    @Transactional\n    fun execute(userId: Long, orderUuid: String, reason: String? = null): AdminOrderResponse {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        order.cancel(orderValidator, reason)\n\n        val userName = order.userId?.let {\n            runCatching { userAdaptor.queryUser(it).profile?.name }.getOrNull()\n        }\n        val eventName = order.eventId?.let {\n            runCatching { eventAdaptor.findById(it).eventBasic?.name }.getOrNull()\n        }\n        return AdminOrderResponse.of(order, userName, eventName)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminChangeNameUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.request.AdminChangeNameRequest\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass AdminChangeNameUseCase(\n    private val adminAuthValidator: AdminAuthValidator,\n    private val userAdaptor: UserAdaptor,\n) {\n\n    @Transactional\n    fun execute(adminUserId: Long, targetUserId: Long, request: AdminChangeNameRequest) {\n        adminAuthValidator.validateAdminOrAbove(adminUserId)\n        val user = userAdaptor.queryUser(targetUserId)\n        user.changeName(request.name)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminCompleteRefundUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminRefundResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass AdminCompleteRefundUseCase(\n    private val orderAdaptor: OrderAdaptor,\n    private val userAdaptor: UserAdaptor,\n    private val eventAdaptor: EventAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    @Transactional\n    fun execute(userId: Long, orderUuid: String): AdminRefundResponse {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        order.completeRefund()\n\n        val userName = order.userId?.let {\n            runCatching { userAdaptor.queryUser(it).profile?.name }.getOrNull()\n        }\n        val eventName = order.eventId?.let {\n            runCatching { eventAdaptor.findById(it).eventBasic?.name }.getOrNull()\n        }\n        return AdminRefundResponse.of(order, userName, eventName)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminDeleteCommentUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.comment.adaptor.CommentAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass AdminDeleteCommentUseCase(\n    private val commentAdaptor: CommentAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    @Transactional\n    fun execute(userId: Long, commentId: Long) {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val comment = commentAdaptor.queryComment(commentId)\n        comment.delete()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminDeleteEventUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.domain.EventStatus\nimport band.gosrock.domain.domains.event.repository.EventRepository\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass AdminDeleteEventUseCase(\n    private val eventAdaptor: EventAdaptor,\n    private val eventRepository: EventRepository,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    @Transactional\n    fun execute(userId: Long, eventId: Long) {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val event = eventAdaptor.findById(eventId)\n        // 어드민은 밸리데이션 없이 직접 DELETED 상태로 변경\n        event.adminUpdateStatus(EventStatus.DELETED)\n        eventRepository.save(event)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminExcelService.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminEventResponse\nimport band.gosrock.admin.model.dto.response.AdminIssuedTicketResponse\nimport band.gosrock.admin.model.dto.response.AdminOrderResponse\nimport band.gosrock.admin.model.dto.response.AdminTicketItemResponse\nimport band.gosrock.admin.model.dto.response.AdminUserResponse\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport band.gosrock.domain.domains.ticket_item.domain.Option\nimport org.apache.poi.xssf.usermodel.XSSFWorkbook\nimport org.springframework.stereotype.Service\nimport java.io.ByteArrayOutputStream\n\n@Service\nclass AdminExcelService {\n\n    fun generateOrdersExcel(orders: List<AdminOrderResponse>): ByteArray {\n        val workbook = XSSFWorkbook()\n        val sheet = workbook.createSheet(\"주문 목록\")\n        val headerRow = sheet.createRow(0)\n        val headers = listOf(\n            \"주문번호\", \"주문번호(읽기용)\", \"사용자\", \"이벤트\", \"티켓\", \"금액\", \"상태\",\n            \"주문방식\", \"결제수단\", \"승인일시\", \"취소일시\", \"할인금액\", \"쿠폰명\", \"주문일\",\n        )\n        headers.forEachIndexed { i, h -> headerRow.createCell(i).setCellValue(h) }\n        orders.forEachIndexed { idx, order ->\n            val row = sheet.createRow(idx + 1)\n            var col = 0\n            row.createCell(col++).setCellValue(order.orderId ?: \"\")\n            row.createCell(col++).setCellValue(order.orderNo ?: \"\")\n            row.createCell(col++).setCellValue(order.userName ?: \"\")\n            row.createCell(col++).setCellValue(order.eventName ?: \"\")\n            row.createCell(col++).setCellValue(order.ticketName ?: \"\")\n            row.createCell(col++).setCellValue(order.totalAmount.toDouble())\n            row.createCell(col++).setCellValue(order.orderStatus.toString())\n            row.createCell(col++).setCellValue(order.orderMethod ?: \"\")\n            row.createCell(col++).setCellValue(order.paymentMethod ?: \"\")\n            row.createCell(col++).setCellValue(order.approvedAt?.toString() ?: \"\")\n            row.createCell(col++).setCellValue(order.withDrawAt?.toString() ?: \"\")\n            row.createCell(col++).setCellValue(order.discountAmount ?: \"\")\n            row.createCell(col++).setCellValue(order.couponName ?: \"\")\n            row.createCell(col++).setCellValue(order.createdAt?.toString() ?: \"\")\n        }\n        return toByteArray(workbook)\n    }\n\n    fun generateEventsExcel(events: List<AdminEventResponse>): ByteArray {\n        val workbook = XSSFWorkbook()\n        val sheet = workbook.createSheet(\"이벤트 목록\")\n        val headerRow = sheet.createRow(0)\n        listOf(\"ID\", \"이벤트명\", \"호스트\", \"상태\", \"시작일\", \"런타임(분)\", \"생성일\").forEachIndexed { i, h ->\n            headerRow.createCell(i).setCellValue(h)\n        }\n        events.forEachIndexed { idx, event ->\n            val row = sheet.createRow(idx + 1)\n            row.createCell(0).setCellValue(event.id.toDouble())\n            row.createCell(1).setCellValue(event.name ?: \"\")\n            row.createCell(2).setCellValue(event.hostName ?: \"\")\n            row.createCell(3).setCellValue(event.status.toString())\n            row.createCell(4).setCellValue(event.startAt?.toString() ?: \"\")\n            row.createCell(5).setCellValue(event.runTime?.toDouble() ?: 0.0)\n            row.createCell(6).setCellValue(event.createdAt?.toString() ?: \"\")\n        }\n        return toByteArray(workbook)\n    }\n\n    fun generateUsersExcel(users: List<AdminUserResponse>): ByteArray {\n        val workbook = XSSFWorkbook()\n        val sheet = workbook.createSheet(\"유저 목록\")\n        val headerRow = sheet.createRow(0)\n        listOf(\"ID\", \"이름\", \"이메일\", \"역할\", \"상태\", \"가입일\").forEachIndexed { i, h ->\n            headerRow.createCell(i).setCellValue(h)\n        }\n        users.forEachIndexed { idx, user ->\n            val row = sheet.createRow(idx + 1)\n            row.createCell(0).setCellValue(user.id.toDouble())\n            row.createCell(1).setCellValue(user.name ?: \"\")\n            row.createCell(2).setCellValue(user.email ?: \"\")\n            row.createCell(3).setCellValue(user.accountRole.toString())\n            row.createCell(4).setCellValue(user.accountState.toString())\n            row.createCell(5).setCellValue(user.createdAt?.toString() ?: \"\")\n        }\n        return toByteArray(workbook)\n    }\n\n    fun generateTicketItemsExcel(items: List<AdminTicketItemResponse>): ByteArray {\n        val workbook = XSSFWorkbook()\n        val sheet = workbook.createSheet(\"티켓 종류 목록\")\n        val headerRow = sheet.createRow(0)\n        listOf(\"이름\", \"설명\", \"가격\", \"수량\", \"판매수\", \"구매제한\", \"타입\", \"상태\").forEachIndexed { i, h ->\n            headerRow.createCell(i).setCellValue(h)\n        }\n        items.forEachIndexed { idx, item ->\n            val row = sheet.createRow(idx + 1)\n            row.createCell(0).setCellValue(item.name ?: \"\")\n            row.createCell(1).setCellValue(item.description ?: \"\")\n            row.createCell(2).setCellValue(item.price?.toDouble() ?: 0.0)\n            row.createCell(3).setCellValue(item.quantity?.toDouble() ?: 0.0)\n            row.createCell(4).setCellValue(item.supplyCount?.toDouble() ?: 0.0)\n            row.createCell(5).setCellValue(item.purchaseLimit?.toDouble() ?: 0.0)\n            row.createCell(6).setCellValue(item.type?.toString() ?: \"\")\n            row.createCell(7).setCellValue(item.ticketItemStatus.toString())\n        }\n        return toByteArray(workbook)\n    }\n\n    fun generateIssuedTicketsExcel(tickets: List<AdminIssuedTicketResponse>): ByteArray {\n        return generateIssuedTicketsExcelWithOptions(tickets, emptyList(), emptyList())\n    }\n\n    /**\n     * 발급 티켓 엑셀에 옵션 응답을 동적 컬럼으로 포함하여 생성합니다.\n     *\n     * @param tickets 발급 티켓 응답 목록\n     * @param options 이벤트에 등록된 옵션 목록 (동적 컬럼 헤더용)\n     * @param issuedTickets 발급 티켓 엔티티 목록 (옵션 응답 데이터 접근용)\n     */\n    fun generateIssuedTicketsExcelWithOptions(\n        tickets: List<AdminIssuedTicketResponse>,\n        options: List<Option>,\n        issuedTickets: List<IssuedTicket>,\n    ): ByteArray {\n        val workbook = XSSFWorkbook()\n        val sheet = workbook.createSheet(\"발급 티켓 목록\")\n        val headerRow = sheet.createRow(0)\n\n        // 기본 헤더\n        val baseHeaders = listOf(\"티켓번호\", \"유저명\", \"티켓종류\", \"주문번호\", \"입장여부\", \"발급일\")\n        baseHeaders.forEachIndexed { i, h -> headerRow.createCell(i).setCellValue(h) }\n\n        // 옵션 동적 헤더\n        val optionHeaders = options.map { it.getQuestionName() ?: \"옵션(${it.id})\" }\n        optionHeaders.forEachIndexed { i, h ->\n            headerRow.createCell(baseHeaders.size + i).setCellValue(h)\n        }\n\n        // IssuedTicket id -> IssuedTicket 맵 (옵션 응답 조회용)\n        val ticketEntityMap = issuedTickets.associateBy { it.id }\n\n        tickets.forEachIndexed { idx, ticket ->\n            val row = sheet.createRow(idx + 1)\n            var col = 0\n            row.createCell(col++).setCellValue(ticket.issuedTicketNo ?: \"\")\n            row.createCell(col++).setCellValue(ticket.userName ?: \"\")\n            row.createCell(col++).setCellValue(ticket.ticketName ?: \"\")\n            row.createCell(col++).setCellValue(ticket.orderUuid ?: \"\")\n            row.createCell(col++).setCellValue(if (ticket.enteredAt != null) \"입장\" else \"미입장\")\n            row.createCell(col++).setCellValue(ticket.createdAt?.toString() ?: \"\")\n\n            // 옵션 응답 채우기\n            if (options.isNotEmpty()) {\n                val entity = ticketEntityMap[ticket.id]\n                val answerMap = entity?.issuedTicketOptionAnswers\n                    ?.associateBy { it.optionId } ?: emptyMap()\n                options.forEach { option ->\n                    val answer = answerMap[option.id]?.answer ?: \"\"\n                    row.createCell(col++).setCellValue(answer)\n                }\n            }\n        }\n        return toByteArray(workbook)\n    }\n\n    private fun toByteArray(workbook: XSSFWorkbook): ByteArray {\n        val out = ByteArrayOutputStream()\n        workbook.write(out)\n        workbook.close()\n        return out.toByteArray()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminExportIssuedTicketsUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.issuedTicket.repository.IssuedTicketRepository\nimport band.gosrock.domain.domains.ticket_item.adaptor.OptionGroupAdaptor\nimport band.gosrock.domain.domains.ticket_item.domain.Option\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass AdminExportIssuedTicketsUseCase(\n    private val issuedTicketRepository: IssuedTicketRepository,\n    private val optionGroupAdaptor: OptionGroupAdaptor,\n    private val adminExcelService: AdminExcelService,\n    private val adminAuthValidator: AdminAuthValidator,\n    private val adminGetIssuedTicketsUseCase: AdminGetIssuedTicketsUseCase,\n) {\n\n    fun execute(userId: Long, eventId: Long): ByteArray {\n        adminAuthValidator.validateAdminOrAbove(userId)\n\n        // 발급 티켓 엔티티 조회\n        val issuedTickets = issuedTicketRepository.findAllByEventId(eventId)\n\n        // 발급 티켓 응답 DTO 변환\n        val ticketResponses = adminGetIssuedTicketsUseCase.executeAll(userId, eventId)\n\n        // 이벤트의 옵션 그룹 → 옵션 목록 조회\n        val optionGroups = optionGroupAdaptor.findAllByEventId(eventId)\n        val options: List<Option> = optionGroups.flatMap { it.options }\n\n        return adminExcelService.generateIssuedTicketsExcelWithOptions(\n            tickets = ticketResponses,\n            options = options,\n            issuedTickets = issuedTickets,\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminGetCommentsUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminCommentResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.comment.repository.CommentRepository\nimport band.gosrock.domain.domains.event.repository.EventRepository\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass AdminGetCommentsUseCase(\n    private val commentRepository: CommentRepository,\n    private val eventRepository: EventRepository,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    fun execute(userId: Long, keyword: String?, eventId: Long?, pageable: Pageable): Page<AdminCommentResponse> {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val commentPage = commentRepository.findAllForAdmin(keyword, eventId, pageable)\n\n        // batch fetch events to avoid N+1\n        val eventIds = commentPage.content.mapNotNull { it.eventId }\n        val eventMap = eventRepository.findAllByIdIn(eventIds).associateBy { it.id }\n\n        return commentPage.map { comment ->\n            val eventName = comment.eventId?.let { eventMap[it]?.eventBasic?.name }\n            AdminCommentResponse.of(comment, eventName)\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminGetEventDetailUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminEventResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.exception.EventNotFoundException\nimport band.gosrock.domain.domains.event.repository.EventRepository\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.issuedTicket.repository.IssuedTicketRepository\nimport band.gosrock.domain.domains.order.repository.OrderRepository\nimport band.gosrock.domain.domains.ticket_item.repository.TicketItemRepository\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass AdminGetEventDetailUseCase(\n    private val eventRepository: EventRepository,\n    private val hostAdaptor: HostAdaptor,\n    private val ticketItemRepository: TicketItemRepository,\n    private val issuedTicketRepository: IssuedTicketRepository,\n    private val orderRepository: OrderRepository,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    fun execute(userId: Long, eventId: Long): AdminEventResponse {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val event = eventRepository.findByIdForAdmin(eventId)\n            ?: throw EventNotFoundException.EXCEPTION\n\n        val hostName = event.hostId?.let {\n            runCatching { hostAdaptor.findById(it).profile?.name }.getOrNull()\n        }\n        val ticketItemCount = ticketItemRepository.countByEventId(eventId).toInt()\n        val issuedTicketCount = issuedTicketRepository.countByEventId(eventId).toInt()\n        val totalOrderCount = orderRepository.countByEventId(eventId).toInt()\n\n        return AdminEventResponse.ofDetail(\n            event = event,\n            hostName = hostName,\n            ticketItemCount = ticketItemCount,\n            issuedTicketCount = issuedTicketCount,\n            totalOrderCount = totalOrderCount,\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminGetEventsUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminEventResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.repository.EventRepository\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass AdminGetEventsUseCase(\n    private val eventRepository: EventRepository,\n    private val hostAdaptor: HostAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    fun executeAll(userId: Long, keyword: String?, status: String?): List<AdminEventResponse> {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        return eventRepository.findAllForAdminNoPage(keyword, status)\n            .map { event ->\n                val hostName = event.hostId?.let {\n                    runCatching { hostAdaptor.findById(it).profile?.name }.getOrNull()\n                }\n                AdminEventResponse.of(event, hostName)\n            }\n    }\n\n    fun execute(userId: Long, keyword: String?, status: String?, pageable: Pageable): Page<AdminEventResponse> {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        return eventRepository.findAllForAdmin(keyword, status, pageable)\n            .map { event ->\n                val hostName = event.hostId?.let {\n                    runCatching { hostAdaptor.findById(it).profile?.name }.getOrNull()\n                }\n                AdminEventResponse.of(event, hostName)\n            }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminGetHostDetailUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminHostDetailResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass AdminGetHostDetailUseCase(\n    private val hostAdaptor: HostAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    fun execute(userId: Long, hostId: Long): AdminHostDetailResponse {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val host = hostAdaptor.findById(hostId)\n        return AdminHostDetailResponse.from(host)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminGetHostEventsUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminEventResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.repository.EventRepository\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass AdminGetHostEventsUseCase(\n    private val eventRepository: EventRepository,\n    private val hostAdaptor: HostAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    fun execute(userId: Long, hostId: Long, pageable: Pageable): Page<AdminEventResponse> {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val host = hostAdaptor.findById(hostId)\n        val hostName = host.profile?.name\n        return eventRepository.findAllByHostId(hostId, pageable)\n            .map { AdminEventResponse.of(it, hostName) }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminGetHostMembersUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminHostMemberResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass AdminGetHostMembersUseCase(\n    private val hostAdaptor: HostAdaptor,\n    private val userAdaptor: UserAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    fun execute(userId: Long, hostId: Long): List<AdminHostMemberResponse> {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val host = hostAdaptor.findById(hostId)\n        val userIds = host.getHostUser_UserIds()\n        val userMap = userAdaptor.queryUserListByIdIn(userIds)\n            .associateBy { it.id }\n\n        return host.hostUsers.map { hostUser ->\n            val userName = hostUser.userId?.let { userMap[it]?.profile?.name }\n            AdminHostMemberResponse.of(hostUser, userName)\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminGetHostsUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminHostResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass AdminGetHostsUseCase(\n    private val hostAdaptor: HostAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    fun execute(userId: Long, keyword: String?, pageable: Pageable): Page<AdminHostResponse> {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        return hostAdaptor.findAllForAdmin(keyword, pageable)\n            .map { AdminHostResponse.from(it) }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminGetIssuedTicketsUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminIssuedTicketResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.issuedTicket.repository.IssuedTicketRepository\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass AdminGetIssuedTicketsUseCase(\n    private val issuedTicketRepository: IssuedTicketRepository,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    fun execute(userId: Long, eventId: Long, pageable: Pageable): Page<AdminIssuedTicketResponse> {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        return issuedTicketRepository.findAllByEventId(eventId, pageable)\n            .map { AdminIssuedTicketResponse.from(it) }\n    }\n\n    fun executeAll(userId: Long, eventId: Long): List<AdminIssuedTicketResponse> {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        return issuedTicketRepository.findAllByEventId(eventId)\n            .map { AdminIssuedTicketResponse.from(it) }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminGetMeUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminUserDetailResponse\nimport band.gosrock.common.annotation.UseCase\n\n@UseCase\nclass AdminGetMeUseCase(\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    fun execute(userId: Long): AdminUserDetailResponse {\n        val user = adminAuthValidator.validateAdminOrAbove(userId)\n        return AdminUserDetailResponse.from(user)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminGetOrderDetailUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminOrderResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass AdminGetOrderDetailUseCase(\n    private val orderAdaptor: OrderAdaptor,\n    private val userAdaptor: UserAdaptor,\n    private val eventAdaptor: EventAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    fun execute(userId: Long, orderUuid: String): AdminOrderResponse {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        val userName = order.userId?.let {\n            runCatching { userAdaptor.queryUser(it).profile?.name }.getOrNull()\n        }\n        val eventName = order.eventId?.let {\n            runCatching { eventAdaptor.findById(it).eventBasic?.name }.getOrNull()\n        }\n        return AdminOrderResponse.of(order, userName, eventName)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminGetOrdersUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminOrderResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.repository.EventRepository\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport band.gosrock.domain.domains.order.repository.OrderRepository\nimport band.gosrock.domain.domains.user.repository.UserRepository\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass AdminGetOrdersUseCase(\n    private val orderRepository: OrderRepository,\n    private val userRepository: UserRepository,\n    private val eventRepository: EventRepository,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    fun executeAll(userId: Long, keyword: String?, status: OrderStatus?, eventId: Long?): List<AdminOrderResponse> {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val orders = orderRepository.findAllForAdminNoPage(keyword, status, eventId)\n        val userIds = orders.mapNotNull { it.userId }\n        val eventIds = orders.mapNotNull { it.eventId }\n        val userMap = userRepository.findAllByIdIn(userIds).associateBy { it.id }\n        val eventMap = eventRepository.findAllByIdIn(eventIds).associateBy { it.id }\n        return orders.map { order ->\n            val userName = order.userId?.let { userMap[it]?.profile?.name }\n            val eventName = order.eventId?.let { eventMap[it]?.eventBasic?.name }\n            AdminOrderResponse.of(order, userName, eventName)\n        }\n    }\n\n    fun execute(userId: Long, keyword: String?, status: OrderStatus?, eventId: Long?, pageable: Pageable): Page<AdminOrderResponse> {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val orderPage = orderRepository.findAllForAdmin(keyword, status, eventId, pageable)\n\n        // batch fetch users and events to avoid N+1\n        val userIds = orderPage.content.mapNotNull { it.userId }\n        val eventIds = orderPage.content.mapNotNull { it.eventId }\n\n        val userMap = userRepository.findAllByIdIn(userIds).associateBy { it.id }\n        val eventMap = eventRepository.findAllByIdIn(eventIds).associateBy { it.id }\n\n        return orderPage.map { order ->\n            val userName = order.userId?.let { userMap[it]?.profile?.name }\n            val eventName = order.eventId?.let { eventMap[it]?.eventBasic?.name }\n            AdminOrderResponse.of(order, userName, eventName)\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminGetRefundDetailUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminRefundResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass AdminGetRefundDetailUseCase(\n    private val orderAdaptor: OrderAdaptor,\n    private val userAdaptor: UserAdaptor,\n    private val eventAdaptor: EventAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    fun execute(userId: Long, orderUuid: String): AdminRefundResponse {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        val userName = order.userId?.let {\n            runCatching { userAdaptor.queryUser(it).profile?.name }.getOrNull()\n        }\n        val eventName = order.eventId?.let {\n            runCatching { eventAdaptor.findById(it).eventBasic?.name }.getOrNull()\n        }\n        return AdminRefundResponse.of(order, userName, eventName)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminGetRefundsUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminRefundResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.repository.EventRepository\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.domain.RefundStatus\nimport band.gosrock.domain.domains.user.repository.UserRepository\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass AdminGetRefundsUseCase(\n    private val orderAdaptor: OrderAdaptor,\n    private val userRepository: UserRepository,\n    private val eventRepository: EventRepository,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    fun execute(\n        userId: Long,\n        refundStatus: RefundStatus?,\n        eventId: Long?,\n        keyword: String?,\n        pageable: Pageable,\n    ): Page<AdminRefundResponse> {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val orderPage = orderAdaptor.findRefunds(eventId, refundStatus, keyword, pageable)\n\n        val userIds = orderPage.content.mapNotNull { it.userId }\n        val eventIds = orderPage.content.mapNotNull { it.eventId }\n\n        val userMap = userRepository.findAllByIdIn(userIds).associateBy { it.id }\n        val eventMap = eventRepository.findAllByIdIn(eventIds).associateBy { it.id }\n\n        return orderPage.map { order ->\n            val userName = order.userId?.let { userMap[it]?.profile?.name }\n            val eventName = order.eventId?.let { eventMap[it]?.eventBasic?.name }\n            AdminRefundResponse.of(order, userName, eventName)\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminGetTicketItemsUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminTicketItemResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass AdminGetTicketItemsUseCase(\n    private val ticketItemAdaptor: TicketItemAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    fun execute(userId: Long, eventId: Long): List<AdminTicketItemResponse> {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        return ticketItemAdaptor.findAllByEventId(eventId)\n            .map { AdminTicketItemResponse.from(it) }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminGetUserDetailUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminUserDetailResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass AdminGetUserDetailUseCase(\n    private val userAdaptor: UserAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    fun execute(userId: Long, targetUserId: Long): AdminUserDetailResponse {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val user = userAdaptor.queryUser(targetUserId)\n        return AdminUserDetailResponse.from(user)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminGetUsersUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminUserResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.user.repository.UserRepository\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass AdminGetUsersUseCase(\n    private val userRepository: UserRepository,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    fun executeAll(userId: Long, keyword: String?): List<AdminUserResponse> {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        return userRepository.findAllByKeywordNoPage(keyword)\n            .map { AdminUserResponse.from(it) }\n    }\n\n    fun execute(userId: Long, keyword: String?, pageable: Pageable): Page<AdminUserResponse> {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        return userRepository.findAllByKeyword(keyword, pageable)\n            .map { AdminUserResponse.from(it) }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminRemoveHostMemberUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.repository.HostRepository\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass AdminRemoveHostMemberUseCase(\n    private val hostAdaptor: HostAdaptor,\n    private val hostRepository: HostRepository,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    @Transactional\n    fun execute(userId: Long, hostId: Long, targetUserId: Long) {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val host = hostAdaptor.findById(hostId)\n        // 어드민이므로 active 여부 체크 없이 강제 제거\n        val hostUser = host.getHostUserByUserId(targetUserId)\n        host.hostUsers.remove(hostUser)\n        hostRepository.save(host)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminTransferMasterUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.request.AdminTransferMasterRequest\nimport band.gosrock.admin.model.dto.response.AdminHostDetailResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.repository.HostRepository\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass AdminTransferMasterUseCase(\n    private val adminAuthValidator: AdminAuthValidator,\n    private val hostAdaptor: HostAdaptor,\n    private val hostRepository: HostRepository,\n) {\n\n    @Transactional\n    fun execute(adminUserId: Long, hostId: Long, request: AdminTransferMasterRequest): AdminHostDetailResponse {\n        adminAuthValidator.validateAdminOrAbove(adminUserId)\n        val host = hostAdaptor.findById(hostId)\n        host.forceTransferMaster(request.newMasterUserId)\n        hostRepository.save(host)\n        return AdminHostDetailResponse.from(host)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminUpdateEventStatusUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.request.AdminUpdateEventStatusRequest\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.domain.EventStatus\nimport band.gosrock.domain.domains.event.repository.EventRepository\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass AdminUpdateEventStatusUseCase(\n    private val eventAdaptor: EventAdaptor,\n    private val eventRepository: EventRepository,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    @Transactional\n    fun execute(userId: Long, eventId: Long, request: AdminUpdateEventStatusRequest) {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val event = eventAdaptor.findById(eventId)\n        // 어드민은 DELETED 제외 모든 상태로 직접 변경 가능 (밸리데이션 우회)\n        require(request.status != EventStatus.DELETED) {\n            \"DELETED 상태는 DELETE 엔드포인트를 사용하세요.\"\n        }\n        event.adminUpdateStatus(request.status)\n        eventRepository.save(event)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminUpdateEventUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.request.AdminUpdateEventRequest\nimport band.gosrock.admin.model.dto.response.AdminEventResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.repository.EventRepository\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass AdminUpdateEventUseCase(\n    private val eventAdaptor: EventAdaptor,\n    private val eventRepository: EventRepository,\n    private val hostAdaptor: HostAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    @Transactional\n    fun execute(userId: Long, eventId: Long, request: AdminUpdateEventRequest): AdminEventResponse {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val event = eventAdaptor.findById(eventId)\n        // 어드민은 OPEN 상태에서도 수정 가능하도록 직접 필드 수정\n        event.adminUpdate(\n            name = request.name,\n            startAt = request.startAt,\n            runTime = request.runTime?.toLong(),\n            content = request.content,\n            placeName = request.placeName,\n            placeAddress = request.placeAddress,\n        )\n        eventRepository.save(event)\n\n        val hostName = event.hostId?.let {\n            runCatching { hostAdaptor.findById(it).profile?.name }.getOrNull()\n        }\n        return AdminEventResponse.of(event, hostName)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminUpdateHostMemberRoleUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.request.AdminUpdateHostMemberRoleRequest\nimport band.gosrock.admin.model.dto.response.AdminHostMemberResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.repository.HostRepository\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass AdminUpdateHostMemberRoleUseCase(\n    private val hostAdaptor: HostAdaptor,\n    private val hostRepository: HostRepository,\n    private val userAdaptor: UserAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    @Transactional\n    fun execute(userId: Long, hostId: Long, targetUserId: Long, request: AdminUpdateHostMemberRoleRequest): AdminHostMemberResponse {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val host = hostAdaptor.findById(hostId)\n        // 어드민이므로 마스터 권한 체크 건너뜀\n        host.setHostUserRole(targetUserId, request.role)\n        hostRepository.save(host)\n\n        val hostUser = host.getHostUserByUserId(targetUserId)\n        val userName = runCatching { userAdaptor.queryUser(targetUserId).profile?.name }.getOrNull()\n        return AdminHostMemberResponse.of(hostUser, userName)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminUpdateHostPartnerUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.request.AdminUpdateHostPartnerRequest\nimport band.gosrock.admin.model.dto.response.AdminHostDetailResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.repository.HostRepository\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass AdminUpdateHostPartnerUseCase(\n    private val hostAdaptor: HostAdaptor,\n    private val hostRepository: HostRepository,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    @Transactional\n    fun execute(userId: Long, hostId: Long, request: AdminUpdateHostPartnerRequest): AdminHostDetailResponse {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val host = hostAdaptor.findById(hostId)\n        host.changePartner(request.partner)\n        hostRepository.save(host)\n        return AdminHostDetailResponse.from(host)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminUpdateHostProfileUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.request.AdminUpdateHostProfileRequest\nimport band.gosrock.admin.model.dto.response.AdminHostDetailResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.domain.HostProfile\nimport band.gosrock.domain.domains.host.repository.HostRepository\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass AdminUpdateHostProfileUseCase(\n    private val hostAdaptor: HostAdaptor,\n    private val hostRepository: HostRepository,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    @Transactional\n    fun execute(userId: Long, hostId: Long, request: AdminUpdateHostProfileRequest): AdminHostDetailResponse {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val host = hostAdaptor.findById(hostId)\n        val current = host.profile\n        val updatedProfile = HostProfile(\n            name = request.name ?: current?.name,\n            introduce = request.introduce ?: current?.introduce,\n            profileImageKey = current?.profileImage?.imageKey,\n            contactEmail = request.contactEmail ?: current?.contactEmail,\n            contactNumber = request.contactNumber ?: current?.contactNumber,\n        )\n        host.updateProfile(updatedProfile)\n        hostRepository.save(host)\n        return AdminHostDetailResponse.from(host)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminUpdateRefundStatusUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.request.AdminRefundStatusRequest\nimport band.gosrock.admin.model.dto.response.AdminOrderResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.domain.RefundStatus\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass AdminUpdateRefundStatusUseCase(\n    private val orderAdaptor: OrderAdaptor,\n    private val userAdaptor: UserAdaptor,\n    private val eventAdaptor: EventAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    @Transactional\n    fun execute(userId: Long, orderUuid: String, request: AdminRefundStatusRequest): AdminOrderResponse {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n\n        when (RefundStatus.valueOf(request.refundStatus)) {\n            RefundStatus.REFUND_COMPLETED -> order.completeRefund()\n            else -> throw IllegalArgumentException(\"허용되지 않는 환불 상태입니다: ${request.refundStatus}\")\n        }\n\n        val userName = order.userId?.let {\n            runCatching { userAdaptor.queryUser(it).profile?.name }.getOrNull()\n        }\n        val eventName = order.eventId?.let {\n            runCatching { eventAdaptor.findById(it).eventBasic?.name }.getOrNull()\n        }\n        return AdminOrderResponse.of(order, userName, eventName)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminUpdateTicketItemUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.request.AdminUpdateTicketItemRequest\nimport band.gosrock.admin.model.dto.response.AdminTicketItemResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass AdminUpdateTicketItemUseCase(\n    private val ticketItemAdaptor: TicketItemAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    @Transactional\n    fun execute(userId: Long, eventId: Long, ticketItemId: Long, request: AdminUpdateTicketItemRequest): AdminTicketItemResponse {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val ticketItem = ticketItemAdaptor.queryTicketItem(ticketItemId)\n        ticketItem.validateEventId(eventId)\n\n        val money = request.price?.let { Money(it) }\n\n        // 어드민은 이벤트 상태 체크 없이 수정 가능\n        ticketItem.adminUpdate(\n            name = request.name,\n            description = request.description,\n            price = money,\n            quantity = request.quantity,\n            purchaseLimit = request.purchaseLimit,\n        )\n        ticketItemAdaptor.save(ticketItem)\n\n        return AdminTicketItemResponse.from(ticketItem)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminUpdateUserRoleUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.request.AdminUpdateUserRoleRequest\nimport band.gosrock.admin.model.dto.response.AdminUserResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.domain.domains.user.repository.UserRepository\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass AdminUpdateUserRoleUseCase(\n    private val userAdaptor: UserAdaptor,\n    private val userRepository: UserRepository,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    @Transactional\n    fun execute(currentUserId: Long, targetUserId: Long, request: AdminUpdateUserRoleRequest): AdminUserResponse {\n        adminAuthValidator.validateSuperAdmin(currentUserId)\n\n        val targetUser = userAdaptor.queryUser(targetUserId)\n        targetUser.changeRole(request.role)\n        userRepository.save(targetUser)\n        return AdminUserResponse.from(targetUser)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/AdminUpdateUserStatusUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.request.AdminUpdateUserStatusRequest\nimport band.gosrock.admin.model.dto.response.AdminUserResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.domain.domains.user.repository.UserRepository\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass AdminUpdateUserStatusUseCase(\n    private val userAdaptor: UserAdaptor,\n    private val userRepository: UserRepository,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    @Transactional\n    fun execute(userId: Long, targetUserId: Long, request: AdminUpdateUserStatusRequest): AdminUserResponse {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val targetUser = userAdaptor.queryUser(targetUserId)\n        targetUser.changeAccountState(request.status)\n        userRepository.save(targetUser)\n        return AdminUserResponse.from(targetUser)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/main/kotlin/band/gosrock/admin/service/GetDashboardUseCase.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminEventResponse\nimport band.gosrock.admin.model.dto.response.AdminOrderResponse\nimport band.gosrock.admin.model.dto.response.DashboardResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.domain.EventStatus\nimport band.gosrock.domain.domains.event.repository.EventRepository\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport band.gosrock.domain.domains.order.repository.OrderRepository\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.domain.domains.user.repository.UserRepository\nimport java.time.LocalDate\nimport java.time.LocalDateTime\nimport org.springframework.data.domain.PageRequest\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass GetDashboardUseCase(\n    private val userRepository: UserRepository,\n    private val orderRepository: OrderRepository,\n    private val eventRepository: EventRepository,\n    private val userAdaptor: UserAdaptor,\n    private val hostAdaptor: HostAdaptor,\n    private val adminAuthValidator: AdminAuthValidator,\n) {\n\n    fun execute(userId: Long, startDate: LocalDate? = null, endDate: LocalDate? = null): DashboardResponse {\n        adminAuthValidator.validateAdminOrAbove(userId)\n        val totalUsers = userRepository.count()\n        val activeEvents = eventRepository.countByStatusNative(EventStatus.OPEN.statusName)\n\n        val periodNewUsers: Long\n        val periodOrders: Long\n        val periodRevenue: Long\n        val periodRefunds: Long\n\n        if (startDate != null && endDate != null) {\n            val start: LocalDateTime = startDate.atStartOfDay()\n            val end: LocalDateTime = endDate.plusDays(1).atStartOfDay()\n\n            periodNewUsers = userRepository.countByCreatedAtBetween(start, end)\n            periodOrders = orderRepository.countByCreatedAtBetween(start, end)\n            periodRefunds = orderRepository.countByCreatedAtBetweenAndOrderStatus(start, end, OrderStatus.REFUND)\n            val confirmedOrders = orderRepository.findByCreatedAtBetweenAndOrderStatusIn(\n                start, end,\n                listOf(OrderStatus.CONFIRM, OrderStatus.APPROVED)\n            )\n            periodRevenue = confirmedOrders.sumOf { it.getTotalPaymentPrice().longValue() }\n        } else {\n            val todayStart: LocalDateTime = LocalDate.now().atStartOfDay()\n\n            periodNewUsers = userRepository.countByCreatedAtAfter(todayStart)\n            periodOrders = orderRepository.countByCreatedAtAfter(todayStart)\n            periodRefunds = orderRepository.countByCreatedAtAfterAndOrderStatus(todayStart, OrderStatus.REFUND)\n            val confirmedOrders = orderRepository.findByCreatedAtAfterAndOrderStatusIn(\n                todayStart,\n                listOf(OrderStatus.CONFIRM, OrderStatus.APPROVED)\n            )\n            periodRevenue = confirmedOrders.sumOf { it.getTotalPaymentPrice().longValue() }\n        }\n\n        // 최근 주문 5건\n        val recentOrders = orderRepository.findTopNByOrderByCreatedAtDesc(PageRequest.of(0, 5))\n            .map { order ->\n                val userName = order.userId?.let {\n                    runCatching { userAdaptor.queryUser(it).profile?.name }.getOrNull()\n                }\n                val eventName = order.eventId?.let {\n                    runCatching { eventRepository.findByIdForAdmin(it)?.eventBasic?.name }.getOrNull()\n                }\n                AdminOrderResponse.of(order, userName, eventName)\n            }\n\n        // 최근 이벤트 5건\n        val recentEvents = eventRepository.findTopNByOrderByCreatedAtDesc(5)\n            .map { event ->\n                val hostName = event.hostId?.let {\n                    runCatching { hostAdaptor.findById(it).profile?.name }.getOrNull()\n                }\n                AdminEventResponse.of(event, hostName)\n            }\n\n        return DashboardResponse(\n            totalUsers = totalUsers,\n            todayNewUsers = periodNewUsers,\n            todayOrders = periodOrders,\n            todayRevenue = periodRevenue,\n            activeEvents = activeEvents,\n            todayRefunds = periodRefunds,\n            recentOrders = recentOrders,\n            recentEvents = recentEvents,\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/test/kotlin/band/gosrock/admin/service/AdminAuthValidatorTest.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.common.exception.DuDoongCodeException\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.domain.domains.user.domain.AccountRole\nimport band.gosrock.domain.domains.user.domain.User\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Assertions.assertNotNull\nimport org.junit.jupiter.api.Assertions.assertThrows\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.DisplayName\nimport org.junit.jupiter.api.Nested\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.extension.ExtendWith\nimport org.mockito.Mock\nimport org.mockito.Mockito.`when`\nimport org.mockito.junit.jupiter.MockitoExtension\nimport org.springframework.test.util.ReflectionTestUtils\n\n@ExtendWith(MockitoExtension::class)\n@DisplayName(\"AdminAuthValidator\")\nclass AdminAuthValidatorTest {\n\n    @Mock\n    private lateinit var userAdaptor: UserAdaptor\n\n    private lateinit var adminAuthValidator: AdminAuthValidator\n\n    @BeforeEach\n    fun setUp() {\n        adminAuthValidator = AdminAuthValidator(userAdaptor)\n    }\n\n    private fun createUser(userId: Long, role: AccountRole): User {\n        val user = User()\n        ReflectionTestUtils.setField(user, \"id\", userId)\n        ReflectionTestUtils.setField(user, \"accountRole\", role)\n        return user\n    }\n\n    @Nested\n    @DisplayName(\"validateAdminOrAbove\")\n    inner class ValidateAdminOrAboveTest {\n\n        @Test\n        @DisplayName(\"USER 역할이면 예외 발생\")\n        fun userRoleDenied() {\n            val user = createUser(1L, AccountRole.USER)\n            `when`(userAdaptor.queryUser(1L)).thenReturn(user)\n\n            assertThrows(DuDoongCodeException::class.java) {\n                adminAuthValidator.validateAdminOrAbove(1L)\n            }\n        }\n\n        @Test\n        @DisplayName(\"MANAGER 역할이면 예외 발생\")\n        fun managerRoleDenied() {\n            val user = createUser(1L, AccountRole.MANAGER)\n            `when`(userAdaptor.queryUser(1L)).thenReturn(user)\n\n            assertThrows(DuDoongCodeException::class.java) {\n                adminAuthValidator.validateAdminOrAbove(1L)\n            }\n        }\n\n        @Test\n        @DisplayName(\"ADMIN 역할이면 허용\")\n        fun adminRoleAllowed() {\n            val user = createUser(1L, AccountRole.ADMIN)\n            `when`(userAdaptor.queryUser(1L)).thenReturn(user)\n\n            val result = adminAuthValidator.validateAdminOrAbove(1L)\n            assertNotNull(result)\n            assertEquals(AccountRole.ADMIN, result.accountRole)\n        }\n\n        @Test\n        @DisplayName(\"SUPER_ADMIN 역할이면 허용\")\n        fun superAdminRoleAllowed() {\n            val user = createUser(1L, AccountRole.SUPER_ADMIN)\n            `when`(userAdaptor.queryUser(1L)).thenReturn(user)\n\n            val result = adminAuthValidator.validateAdminOrAbove(1L)\n            assertNotNull(result)\n            assertEquals(AccountRole.SUPER_ADMIN, result.accountRole)\n        }\n    }\n\n    @Nested\n    @DisplayName(\"validateSuperAdmin\")\n    inner class ValidateSuperAdminTest {\n\n        @Test\n        @DisplayName(\"USER 역할이면 예외 발생\")\n        fun userRoleDenied() {\n            val user = createUser(1L, AccountRole.USER)\n            `when`(userAdaptor.queryUser(1L)).thenReturn(user)\n\n            assertThrows(DuDoongCodeException::class.java) {\n                adminAuthValidator.validateSuperAdmin(1L)\n            }\n        }\n\n        @Test\n        @DisplayName(\"MANAGER 역할이면 예외 발생\")\n        fun managerRoleDenied() {\n            val user = createUser(1L, AccountRole.MANAGER)\n            `when`(userAdaptor.queryUser(1L)).thenReturn(user)\n\n            assertThrows(DuDoongCodeException::class.java) {\n                adminAuthValidator.validateSuperAdmin(1L)\n            }\n        }\n\n        @Test\n        @DisplayName(\"ADMIN 역할이면 예외 발생\")\n        fun adminRoleDenied() {\n            val user = createUser(1L, AccountRole.ADMIN)\n            `when`(userAdaptor.queryUser(1L)).thenReturn(user)\n\n            assertThrows(DuDoongCodeException::class.java) {\n                adminAuthValidator.validateSuperAdmin(1L)\n            }\n        }\n\n        @Test\n        @DisplayName(\"SUPER_ADMIN 역할이면 허용\")\n        fun superAdminRoleAllowed() {\n            val user = createUser(1L, AccountRole.SUPER_ADMIN)\n            `when`(userAdaptor.queryUser(1L)).thenReturn(user)\n\n            val result = adminAuthValidator.validateSuperAdmin(1L)\n            assertNotNull(result)\n            assertEquals(AccountRole.SUPER_ADMIN, result.accountRole)\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/test/kotlin/band/gosrock/admin/service/AdminChangeNameUseCaseTest.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.request.AdminChangeNameRequest\nimport band.gosrock.common.exception.DuDoongCodeException\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.domain.domains.user.domain.AccountRole\nimport band.gosrock.domain.domains.user.domain.Profile\nimport band.gosrock.domain.domains.user.domain.User\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Assertions.assertThrows\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.DisplayName\nimport org.junit.jupiter.api.Nested\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.extension.ExtendWith\nimport org.mockito.Mock\nimport org.mockito.Mockito.`when`\nimport org.mockito.junit.jupiter.MockitoExtension\nimport org.springframework.test.util.ReflectionTestUtils\n\n@ExtendWith(MockitoExtension::class)\n@DisplayName(\"AdminChangeNameUseCase\")\nclass AdminChangeNameUseCaseTest {\n\n    @Mock\n    private lateinit var userAdaptor: UserAdaptor\n\n    private lateinit var adminAuthValidator: AdminAuthValidator\n    private lateinit var adminChangeNameUseCase: AdminChangeNameUseCase\n\n    @BeforeEach\n    fun setUp() {\n        adminAuthValidator = AdminAuthValidator(userAdaptor)\n        adminChangeNameUseCase = AdminChangeNameUseCase(adminAuthValidator, userAdaptor)\n    }\n\n    private fun createUser(userId: Long, role: AccountRole, name: String = \"기존이름\"): User {\n        val user = User(profile = Profile(name = name))\n        ReflectionTestUtils.setField(user, \"id\", userId)\n        ReflectionTestUtils.setField(user, \"accountRole\", role)\n        return user\n    }\n\n    @Nested\n    @DisplayName(\"execute\")\n    inner class ExecuteTest {\n\n        @Test\n        @DisplayName(\"ADMIN이 유저 이름을 변경하면 성공한다\")\n        fun adminCanChangeName() {\n            val admin = createUser(1L, AccountRole.ADMIN)\n            val target = createUser(2L, AccountRole.USER, \"기존이름\")\n            `when`(userAdaptor.queryUser(1L)).thenReturn(admin)\n            `when`(userAdaptor.queryUser(2L)).thenReturn(target)\n\n            val request = AdminChangeNameRequest(name = \"새이름입니다\")\n            adminChangeNameUseCase.execute(1L, 2L, request)\n\n            assertEquals(\"새이름입니다\", target.profile?.name)\n        }\n\n        @Test\n        @DisplayName(\"SUPER_ADMIN이 유저 이름을 변경하면 성공한다\")\n        fun superAdminCanChangeName() {\n            val superAdmin = createUser(1L, AccountRole.SUPER_ADMIN)\n            val target = createUser(2L, AccountRole.USER, \"기존이름\")\n            `when`(userAdaptor.queryUser(1L)).thenReturn(superAdmin)\n            `when`(userAdaptor.queryUser(2L)).thenReturn(target)\n\n            val request = AdminChangeNameRequest(name = \"변경됨\")\n            adminChangeNameUseCase.execute(1L, 2L, request)\n\n            assertEquals(\"변경됨\", target.profile?.name)\n        }\n\n        @Test\n        @DisplayName(\"USER 역할이면 예외가 발생한다\")\n        fun userRoleDenied() {\n            val user = createUser(1L, AccountRole.USER)\n            `when`(userAdaptor.queryUser(1L)).thenReturn(user)\n\n            val request = AdminChangeNameRequest(name = \"새이름입니다\")\n            assertThrows(DuDoongCodeException::class.java) {\n                adminChangeNameUseCase.execute(1L, 2L, request)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/test/kotlin/band/gosrock/admin/service/AdminCompleteRefundUseCaseTest.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderMethod\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport band.gosrock.domain.domains.order.domain.RefundStatus\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Assertions.assertNotNull\nimport org.junit.jupiter.api.DisplayName\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.extension.ExtendWith\nimport org.mockito.BDDMockito.given\nimport org.mockito.InjectMocks\nimport org.mockito.Mock\nimport org.mockito.junit.jupiter.MockitoExtension\n\n@ExtendWith(MockitoExtension::class)\n@DisplayName(\"AdminCompleteRefundUseCase\")\nclass AdminCompleteRefundUseCaseTest {\n\n    @Mock\n    private lateinit var orderAdaptor: OrderAdaptor\n\n    @Mock\n    private lateinit var userAdaptor: UserAdaptor\n\n    @Mock\n    private lateinit var eventAdaptor: EventAdaptor\n\n    @Mock\n    private lateinit var adminAuthValidator: AdminAuthValidator\n\n    @InjectMocks\n    private lateinit var adminCompleteRefundUseCase: AdminCompleteRefundUseCase\n\n    @Test\n    @DisplayName(\"어드민 환불 확인 시 refundStatus가 REFUND_COMPLETED로 변경된다\")\n    fun completeRefund() {\n        // given\n        val order = Order.forTest(\n            userId = 1L,\n            orderName = \"테스트주문\",\n            orderStatus = OrderStatus.CANCELED,\n            orderMethod = OrderMethod.PAYMENT,\n            eventId = 100L,\n            cancelReason = \"단순 변심\",\n            refundStatus = RefundStatus.REFUND_REQUESTED,\n        )\n        given(orderAdaptor.findByOrderUuid(\"test-uuid\")).willReturn(order)\n\n        // when\n        val response = adminCompleteRefundUseCase.execute(1L, \"test-uuid\")\n\n        // then\n        assertEquals(RefundStatus.REFUND_COMPLETED, response.refundStatus)\n        assertEquals(RefundStatus.REFUND_COMPLETED, order.refundStatus)\n        assertNotNull(order.refundStatusChangedAt)\n        assertNotNull(response.userId)\n        assertEquals(1L, response.userId)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/test/kotlin/band/gosrock/admin/service/AdminExcelServiceTest.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.response.AdminIssuedTicketResponse\nimport band.gosrock.admin.model.dto.response.AdminOrderResponse\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketOptionAnswer\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketStatus\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport band.gosrock.domain.domains.ticket_item.domain.Option\nimport band.gosrock.domain.domains.ticket_item.domain.OptionGroup\nimport band.gosrock.domain.domains.ticket_item.domain.OptionGroupType\nimport org.apache.poi.xssf.usermodel.XSSFWorkbook\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.DisplayName\nimport org.junit.jupiter.api.Nested\nimport org.junit.jupiter.api.Test\nimport org.springframework.test.util.ReflectionTestUtils\nimport java.io.ByteArrayInputStream\nimport java.time.LocalDateTime\n\n@DisplayName(\"AdminExcelService\")\nclass AdminExcelServiceTest {\n\n    private lateinit var adminExcelService: AdminExcelService\n\n    @BeforeEach\n    fun setUp() {\n        adminExcelService = AdminExcelService()\n    }\n\n    @Nested\n    @DisplayName(\"generateOrdersExcel\")\n    inner class GenerateOrdersExcelTest {\n\n        @Test\n        @DisplayName(\"주문 엑셀 헤더 컬럼이 올바르게 생성된다\")\n        fun headerColumnsAreCorrect() {\n            val bytes = adminExcelService.generateOrdersExcel(emptyList())\n            val workbook = XSSFWorkbook(ByteArrayInputStream(bytes))\n            val sheet = workbook.getSheetAt(0)\n            val headerRow = sheet.getRow(0)\n\n            val expectedHeaders = listOf(\n                \"주문번호\", \"주문번호(읽기용)\", \"사용자\", \"이벤트\", \"티켓\", \"금액\", \"상태\",\n                \"주문방식\", \"결제수단\", \"승인일시\", \"취소일시\", \"할인금액\", \"쿠폰명\", \"주문일\",\n            )\n            expectedHeaders.forEachIndexed { i, expected ->\n                assertEquals(expected, headerRow.getCell(i).stringCellValue, \"헤더[$i]\")\n            }\n            assertEquals(expectedHeaders.size, headerRow.lastCellNum.toInt())\n            workbook.close()\n        }\n\n        @Test\n        @DisplayName(\"주문 데이터 행이 올바르게 생성된다\")\n        fun dataRowsAreCorrect() {\n            val now = LocalDateTime.of(2026, 3, 22, 10, 0)\n            val orders = listOf(\n                AdminOrderResponse(\n                    orderId = \"uuid-1\",\n                    orderNo = \"R100001\",\n                    userName = \"테스트유저\",\n                    eventName = \"테스트이벤트\",\n                    ticketName = \"일반 티켓\",\n                    totalAmount = 10000L,\n                    orderStatus = OrderStatus.CONFIRM,\n                    createdAt = now,\n                    orderMethod = \"PAYMENT\",\n                    userId = 1L,\n                    eventId = 1L,\n                    approvedAt = now.plusMinutes(5),\n                    withDrawAt = null,\n                    paymentMethod = \"CARD\",\n                    receiptUrl = \"https://example.com/receipt\",\n                    supplyAmount = \"10000\",\n                    discountAmount = \"1000\",\n                    couponName = \"첫주문쿠폰\",\n                ),\n            )\n\n            val bytes = adminExcelService.generateOrdersExcel(orders)\n            val workbook = XSSFWorkbook(ByteArrayInputStream(bytes))\n            val sheet = workbook.getSheetAt(0)\n            val dataRow = sheet.getRow(1)\n\n            assertEquals(\"uuid-1\", dataRow.getCell(0).stringCellValue)\n            assertEquals(\"R100001\", dataRow.getCell(1).stringCellValue)\n            assertEquals(\"테스트유저\", dataRow.getCell(2).stringCellValue)\n            assertEquals(\"테스트이벤트\", dataRow.getCell(3).stringCellValue)\n            assertEquals(\"일반 티켓\", dataRow.getCell(4).stringCellValue)\n            assertEquals(10000.0, dataRow.getCell(5).numericCellValue)\n            assertEquals(\"CONFIRM\", dataRow.getCell(6).stringCellValue)\n            assertEquals(\"PAYMENT\", dataRow.getCell(7).stringCellValue)\n            assertEquals(\"CARD\", dataRow.getCell(8).stringCellValue)\n            assertEquals(now.plusMinutes(5).toString(), dataRow.getCell(9).stringCellValue)\n            assertEquals(\"\", dataRow.getCell(10).stringCellValue) // withDrawAt null\n            assertEquals(\"1000\", dataRow.getCell(11).stringCellValue)\n            assertEquals(\"첫주문쿠폰\", dataRow.getCell(12).stringCellValue)\n            assertEquals(now.toString(), dataRow.getCell(13).stringCellValue)\n            workbook.close()\n        }\n    }\n\n    @Nested\n    @DisplayName(\"generateIssuedTicketsExcel\")\n    inner class GenerateIssuedTicketsExcelTest {\n\n        @Test\n        @DisplayName(\"기본 발급 티켓 엑셀 헤더가 올바르게 생성된다\")\n        fun basicHeaderColumnsAreCorrect() {\n            val bytes = adminExcelService.generateIssuedTicketsExcel(emptyList())\n            val workbook = XSSFWorkbook(ByteArrayInputStream(bytes))\n            val sheet = workbook.getSheetAt(0)\n            val headerRow = sheet.getRow(0)\n\n            val expectedHeaders = listOf(\"티켓번호\", \"유저명\", \"티켓종류\", \"주문번호\", \"입장여부\", \"발급일\")\n            expectedHeaders.forEachIndexed { i, expected ->\n                assertEquals(expected, headerRow.getCell(i).stringCellValue, \"헤더[$i]\")\n            }\n            assertEquals(expectedHeaders.size, headerRow.lastCellNum.toInt())\n            workbook.close()\n        }\n    }\n\n    @Nested\n    @DisplayName(\"generateIssuedTicketsExcelWithOptions\")\n    inner class GenerateIssuedTicketsExcelWithOptionsTest {\n\n        @Test\n        @DisplayName(\"옵션 동적 컬럼이 헤더에 추가된다\")\n        fun optionDynamicHeadersAreAdded() {\n            val optionGroup = createOptionGroup(1L, \"이름 수집용\", OptionGroupType.SUBJECTIVE)\n            val option1 = createOption(10L, \"참석자 이름\", optionGroup)\n            val option2 = createOption(11L, \"연락처\", optionGroup)\n\n            val bytes = adminExcelService.generateIssuedTicketsExcelWithOptions(\n                tickets = emptyList(),\n                options = listOf(option1, option2),\n                issuedTickets = emptyList(),\n            )\n            val workbook = XSSFWorkbook(ByteArrayInputStream(bytes))\n            val sheet = workbook.getSheetAt(0)\n            val headerRow = sheet.getRow(0)\n\n            // 기본 6 컬럼 + 옵션 2 컬럼\n            assertEquals(8, headerRow.lastCellNum.toInt())\n            assertEquals(\"참석자 이름\", headerRow.getCell(6).stringCellValue)\n            assertEquals(\"연락처\", headerRow.getCell(7).stringCellValue)\n            workbook.close()\n        }\n\n        @Test\n        @DisplayName(\"옵션 응답이 올바르게 매핑된다\")\n        fun optionAnswersAreMapped() {\n            val optionGroup = createOptionGroup(1L, \"참석자 정보\", OptionGroupType.SUBJECTIVE)\n            val option1 = createOption(10L, \"참석자 이름\", optionGroup)\n            val option2 = createOption(11L, \"연락처\", optionGroup)\n\n            val now = LocalDateTime.of(2026, 3, 22, 10, 0)\n\n            // IssuedTicket 엔티티 생성 (옵션 응답 포함)\n            val answer1 = IssuedTicketOptionAnswer(optionId = 10L, answer = \"홍길동\")\n            val answer2 = IssuedTicketOptionAnswer(optionId = 11L, answer = \"010-1234-5678\")\n            val issuedTicket = createIssuedTicket(100L, listOf(answer1, answer2))\n\n            val ticketResponse = AdminIssuedTicketResponse(\n                id = 100L,\n                issuedTicketNo = \"T100001\",\n                userName = \"테스트유저\",\n                ticketName = \"일반 티켓\",\n                orderUuid = \"order-uuid-1\",\n                enteredAt = null,\n                status = IssuedTicketStatus.ENTRANCE_INCOMPLETE,\n                createdAt = now,\n            )\n\n            val bytes = adminExcelService.generateIssuedTicketsExcelWithOptions(\n                tickets = listOf(ticketResponse),\n                options = listOf(option1, option2),\n                issuedTickets = listOf(issuedTicket),\n            )\n            val workbook = XSSFWorkbook(ByteArrayInputStream(bytes))\n            val sheet = workbook.getSheetAt(0)\n            val dataRow = sheet.getRow(1)\n\n            // 기본 컬럼 확인\n            assertEquals(\"T100001\", dataRow.getCell(0).stringCellValue)\n            assertEquals(\"테스트유저\", dataRow.getCell(1).stringCellValue)\n            assertEquals(\"일반 티켓\", dataRow.getCell(2).stringCellValue)\n            assertEquals(\"order-uuid-1\", dataRow.getCell(3).stringCellValue)\n            assertEquals(\"미입장\", dataRow.getCell(4).stringCellValue)\n\n            // 옵션 응답 컬럼 확인\n            assertEquals(\"홍길동\", dataRow.getCell(6).stringCellValue)\n            assertEquals(\"010-1234-5678\", dataRow.getCell(7).stringCellValue)\n            workbook.close()\n        }\n\n        @Test\n        @DisplayName(\"옵션 응답이 없는 경우 빈 문자열로 채워진다\")\n        fun missingOptionAnswersAreFilled() {\n            val optionGroup = createOptionGroup(1L, \"참석자 정보\", OptionGroupType.SUBJECTIVE)\n            val option1 = createOption(10L, \"참석자 이름\", optionGroup)\n            val option2 = createOption(11L, \"연락처\", optionGroup)\n\n            val now = LocalDateTime.of(2026, 3, 22, 10, 0)\n\n            // 옵션 응답이 하나만 있는 경우\n            val answer1 = IssuedTicketOptionAnswer(optionId = 10L, answer = \"홍길동\")\n            val issuedTicket = createIssuedTicket(100L, listOf(answer1))\n\n            val ticketResponse = AdminIssuedTicketResponse(\n                id = 100L,\n                issuedTicketNo = \"T100001\",\n                userName = \"테스트유저\",\n                ticketName = \"일반 티켓\",\n                orderUuid = \"order-uuid-1\",\n                enteredAt = null,\n                status = IssuedTicketStatus.ENTRANCE_INCOMPLETE,\n                createdAt = now,\n            )\n\n            val bytes = adminExcelService.generateIssuedTicketsExcelWithOptions(\n                tickets = listOf(ticketResponse),\n                options = listOf(option1, option2),\n                issuedTickets = listOf(issuedTicket),\n            )\n            val workbook = XSSFWorkbook(ByteArrayInputStream(bytes))\n            val sheet = workbook.getSheetAt(0)\n            val dataRow = sheet.getRow(1)\n\n            assertEquals(\"홍길동\", dataRow.getCell(6).stringCellValue)\n            assertEquals(\"\", dataRow.getCell(7).stringCellValue) // 응답 없음\n            workbook.close()\n        }\n    }\n\n    // --- Helper Methods ---\n\n    private fun createOptionGroup(id: Long, name: String, type: OptionGroupType): OptionGroup {\n        val optionGroup = OptionGroup(\n            eventId = 1L,\n            type = type,\n            name = name,\n            description = \"설명\",\n            isEssential = true,\n        )\n        ReflectionTestUtils.setField(optionGroup, \"id\", id)\n        return optionGroup\n    }\n\n    private fun createOption(id: Long, questionName: String, optionGroup: OptionGroup): Option {\n        val og = OptionGroup(\n            eventId = 1L,\n            type = optionGroup.type,\n            name = questionName,\n            description = optionGroup.description,\n            isEssential = optionGroup.isEssential,\n        )\n        ReflectionTestUtils.setField(og, \"id\", optionGroup.id)\n        val option = Option(answer = \"\", additionalPrice = Money.ZERO, optionGroup = og)\n        ReflectionTestUtils.setField(option, \"id\", id)\n        return option\n    }\n\n    private fun createIssuedTicket(id: Long, optionAnswers: List<IssuedTicketOptionAnswer>): IssuedTicket {\n        val ticket = IssuedTicket(\n            eventId = 1L,\n            initialOptionAnswers = optionAnswers,\n        )\n        ReflectionTestUtils.setField(ticket, \"id\", id)\n        return ticket\n    }\n}\n"
  },
  {
    "path": "DuDoong-Admin/src/test/kotlin/band/gosrock/admin/service/AdminUpdateRefundStatusUseCaseTest.kt",
    "content": "package band.gosrock.admin.service\n\nimport band.gosrock.admin.model.dto.request.AdminRefundStatusRequest\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderMethod\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport band.gosrock.domain.domains.order.domain.RefundStatus\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.domain.domains.user.domain.AccountRole\nimport band.gosrock.domain.domains.user.domain.User\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Assertions.assertNotNull\nimport org.junit.jupiter.api.Assertions.assertThrows\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.DisplayName\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.extension.ExtendWith\nimport org.mockito.Mock\nimport org.mockito.Mockito.`when`\nimport org.mockito.junit.jupiter.MockitoExtension\nimport org.springframework.test.util.ReflectionTestUtils\n\n@ExtendWith(MockitoExtension::class)\n@DisplayName(\"AdminUpdateRefundStatusUseCase\")\nclass AdminUpdateRefundStatusUseCaseTest {\n\n    @Mock\n    private lateinit var orderAdaptor: OrderAdaptor\n\n    @Mock\n    private lateinit var userAdaptor: UserAdaptor\n\n    @Mock\n    private lateinit var eventAdaptor: EventAdaptor\n\n    @Mock\n    private lateinit var adminAuthValidator: AdminAuthValidator\n\n    private lateinit var useCase: AdminUpdateRefundStatusUseCase\n\n    private lateinit var order: Order\n\n    @BeforeEach\n    fun setUp() {\n        useCase = AdminUpdateRefundStatusUseCase(orderAdaptor, userAdaptor, eventAdaptor, adminAuthValidator)\n        order = Order.forTest(\n            userId = 10L,\n            orderName = \"테스트주문\",\n            orderStatus = OrderStatus.CANCELED,\n            orderMethod = OrderMethod.PAYMENT,\n            eventId = 100L,\n            cancelReason = \"단순 변심\",\n            refundStatus = RefundStatus.REFUND_REQUESTED,\n        )\n    }\n\n    private fun createAdminUser(): User {\n        val user = User()\n        ReflectionTestUtils.setField(user, \"id\", 1L)\n        ReflectionTestUtils.setField(user, \"accountRole\", AccountRole.ADMIN)\n        return user\n    }\n\n    @Test\n    @DisplayName(\"환불 완료 처리 시 refundStatus가 REFUND_COMPLETED로 변경된다\")\n    fun completeRefund() {\n        val adminUser = createAdminUser()\n        `when`(adminAuthValidator.validateAdminOrAbove(1L)).thenReturn(adminUser)\n        `when`(orderAdaptor.findByOrderUuid(\"test-uuid\")).thenReturn(order)\n\n        ReflectionTestUtils.setField(order, \"uuid\", \"test-uuid\")\n        val request = AdminRefundStatusRequest(refundStatus = \"REFUND_COMPLETED\")\n\n        useCase.execute(1L, \"test-uuid\", request)\n\n        assertEquals(RefundStatus.REFUND_COMPLETED, order.refundStatus)\n        assertNotNull(order.refundStatusChangedAt)\n    }\n\n    @Test\n    @DisplayName(\"허용되지 않는 refundStatus 값이면 예외가 발생한다\")\n    fun invalidRefundStatus() {\n        val adminUser = createAdminUser()\n        `when`(adminAuthValidator.validateAdminOrAbove(1L)).thenReturn(adminUser)\n        `when`(orderAdaptor.findByOrderUuid(\"test-uuid\")).thenReturn(order)\n\n        ReflectionTestUtils.setField(order, \"uuid\", \"test-uuid\")\n        val request = AdminRefundStatusRequest(refundStatus = \"NONE\")\n\n        assertThrows(IllegalArgumentException::class.java) {\n            useCase.execute(1L, \"test-uuid\", request)\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/Dockerfile",
    "content": "FROM eclipse-temurin:21-jre-alpine\n\nEXPOSE 8080\n\nCOPY ./build/libs/*.jar app.jar\nARG PROFILE=prod\nENV PROFILE=${PROFILE}\n\nENTRYPOINT [\"java\",\"-Dspring.profiles.active=${PROFILE}\", \"-Djava.security.egd=file:/dev/./urandom\",\"-jar\",\"/app.jar\"]"
  },
  {
    "path": "DuDoong-Api/build.gradle.kts",
    "content": "dependencies {\n    implementation(\"org.springframework.boot:spring-boot-starter-web\")\n    implementation(\"org.springframework.boot:spring-boot-starter-validation\")\n    implementation(\"org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0\")\n    implementation(\"org.springframework.boot:spring-boot-starter-security\")\n    implementation(project(\":DuDoong-Domain\"))\n    implementation(project(\":DuDoong-Common\"))\n    implementation(project(\":DuDoong-Infrastructure\"))\n    implementation(project(\":DuDoong-Admin\"))\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/DuDoongApiServerApplication.kt",
    "content": "package band.gosrock\n\nimport org.slf4j.LoggerFactory\nimport org.springframework.boot.SpringApplication\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.boot.context.event.ApplicationReadyEvent\nimport org.springframework.context.ApplicationListener\nimport org.springframework.context.annotation.Bean\nimport org.springframework.core.env.Environment\nimport org.springframework.web.filter.ForwardedHeaderFilter\n\n@SpringBootApplication\nclass DuDoongApiServerApplication(\n    private val environment: Environment\n) : ApplicationListener<ApplicationReadyEvent> {\n\n    private val log = LoggerFactory.getLogger(DuDoongApiServerApplication::class.java)\n\n    @Bean\n    fun forwardedHeaderFilter(): ForwardedHeaderFilter = ForwardedHeaderFilter()\n\n    override fun onApplicationEvent(event: ApplicationReadyEvent) {\n        log.info(\"applicationReady status${environment.activeProfiles.contentToString()}\")\n    }\n}\n\nfun main(args: Array<String>) {\n    SpringApplication.run(DuDoongApiServerApplication::class.java, *args)\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/admin/controller/AdminAuthController.kt",
    "content": "package band.gosrock.api.admin.controller\n\nimport band.gosrock.admin.model.dto.response.AdminUserDetailResponse\nimport band.gosrock.admin.service.AdminGetMeUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RestController\n\n@RestController\n@RequestMapping(\"/internal-api/v1/auth\")\n@Tag(name = \"Admin Auth\")\nclass AdminAuthController(\n    private val adminGetMeUseCase: AdminGetMeUseCase,\n) {\n\n    @Operation(summary = \"현재 어드민 유저 정보 조회\")\n    @SecurityRequirement(name = \"admin-token\")\n    @GetMapping(\"/me\")\n    fun getAdminMe(@CurrentUserId userId: Long): AdminUserDetailResponse {\n        return adminGetMeUseCase.execute(userId)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/alimTalk/dto/OrderAlimTalkDto.kt",
    "content": "package band.gosrock.api.alimTalk.dto\n\nimport band.gosrock.infrastructure.config.alilmTalk.dto.AlimTalkEventInfo\nimport band.gosrock.infrastructure.config.alilmTalk.dto.AlimTalkOrderInfo\nimport band.gosrock.infrastructure.config.alilmTalk.dto.AlimTalkUserInfo\n\ndata class OrderAlimTalkDto(\n    val userInfo: AlimTalkUserInfo,\n    val orderInfo: AlimTalkOrderInfo,\n    val eventInfo: AlimTalkEventInfo,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/alimTalk/handler/DoneOrderEventAlimTalkHandler.kt",
    "content": "package band.gosrock.api.alimTalk.handler\n\nimport band.gosrock.api.alimTalk.service.SendDoneOrderAlimTalkService\nimport band.gosrock.api.alimTalk.service.helper.OrderAlimTalkInfoHelper\nimport band.gosrock.domain.common.events.order.DoneOrderEvent\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.annotation.Propagation\nimport org.springframework.transaction.annotation.Transactional\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\n@Component\nclass DoneOrderEventAlimTalkHandler(\n    private val sendDoneOrderAlimTalkService: SendDoneOrderAlimTalkService,\n    private val orderAlimTalkInfoHelper: OrderAlimTalkInfoHelper,\n    private val orderAdaptor: OrderAdaptor,\n    private val eventAdaptor: EventAdaptor,\n    private val hostAdaptor: HostAdaptor,\n) {\n    private val log = LoggerFactory.getLogger(DoneOrderEventAlimTalkHandler::class.java)\n\n    @Async\n    @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)\n    @TransactionalEventListener(classes = [DoneOrderEvent::class], phase = TransactionPhase.AFTER_COMMIT)\n    fun handleDoneOrderEvent(doneOrderEvent: DoneOrderEvent) {\n        try {\n            log.info(\"${doneOrderEvent.orderUuid}주문 상태 완료, 파트너의 공연이면 알림톡 전송\")\n            val order = orderAdaptor.findByOrderUuid(doneOrderEvent.orderUuid)\n            val event = eventAdaptor.findById(order.eventId!!)\n            val host = hostAdaptor.findById(event.hostId!!)\n            if (host.isPartnerHost()) {\n                val orderAlimTalkDto = orderAlimTalkInfoHelper.execute(order, event, host)\n                sendDoneOrderAlimTalkService.execute(orderAlimTalkDto)\n                log.info(\"${doneOrderEvent.orderUuid}주문 상태 완료, 파트너의 공연 알림톡 전송 완료\")\n            }\n        } catch (e: Exception) {\n            log.warn(\"주문 완료 알림톡 전송 실패 (orderUuid=${doneOrderEvent.orderUuid}): ${e.message}\")\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/alimTalk/handler/RegisterUserEventAlimTalkHandler.kt",
    "content": "package band.gosrock.api.alimTalk.handler\n\nimport band.gosrock.api.alimTalk.service.SendRegisterAlimTalkService\nimport band.gosrock.domain.common.events.user.UserRegisterEvent\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\n@Component\nclass RegisterUserEventAlimTalkHandler(\n    private val userAdaptor: UserAdaptor,\n    private val sendRegisterAlimTalkService: SendRegisterAlimTalkService,\n) {\n    private val log = LoggerFactory.getLogger(RegisterUserEventAlimTalkHandler::class.java)\n\n    @Async\n    @TransactionalEventListener(classes = [UserRegisterEvent::class], phase = TransactionPhase.AFTER_COMMIT)\n    fun handleRegisterUserEvent(userRegisterEvent: UserRegisterEvent) {\n        val userId = userRegisterEvent.userId\n        try {\n            val user = userAdaptor.queryUser(userId)\n            log.info(\"${userId}유저 등록\")\n            val userInfo = user.toAlimTalkUserInfo()\n            sendRegisterAlimTalkService.execute(userInfo.userName, userInfo.phoneNum)\n        } catch (e: Exception) {\n            log.warn(\"유저 등록 알림톡 전송 실패 (userId=$userId): ${e.message}\")\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/alimTalk/handler/WithDrawOrderEventAlimTalkHandler.kt",
    "content": "package band.gosrock.api.alimTalk.handler\n\nimport band.gosrock.api.alimTalk.service.SendWithdrawOrderAlimTalkService\nimport band.gosrock.api.alimTalk.service.helper.OrderAlimTalkInfoHelper\nimport band.gosrock.domain.common.events.order.WithDrawOrderEvent\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.annotation.Propagation\nimport org.springframework.transaction.annotation.Transactional\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\n@Component\nclass WithDrawOrderEventAlimTalkHandler(\n    private val sendWithdrawOrderAlimTalkService: SendWithdrawOrderAlimTalkService,\n    private val orderAlimTalkInfoHelper: OrderAlimTalkInfoHelper,\n    private val orderAdaptor: OrderAdaptor,\n    private val eventAdaptor: EventAdaptor,\n    private val hostAdaptor: HostAdaptor,\n) {\n    private val log = org.slf4j.LoggerFactory.getLogger(WithDrawOrderEventAlimTalkHandler::class.java)\n\n    @Async\n    @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)\n    @TransactionalEventListener(classes = [WithDrawOrderEvent::class], phase = TransactionPhase.AFTER_COMMIT)\n    fun handleWithDrawOrderEvent(withDrawOrderEvent: WithDrawOrderEvent) {\n        try {\n            val order = orderAdaptor.findByOrderUuid(withDrawOrderEvent.orderUuid)\n            val event = eventAdaptor.findById(order.eventId!!)\n            val host = hostAdaptor.findById(event.hostId!!)\n            if (host.isPartnerHost()) {\n                val orderAlimTalkDto = orderAlimTalkInfoHelper.execute(order, event, host)\n                sendWithdrawOrderAlimTalkService.execute(orderAlimTalkDto)\n            }\n        } catch (e: Exception) {\n            log.warn(\"주문 취소 알림톡 전송 실패 (orderUuid=${withDrawOrderEvent.orderUuid}): ${e.message}\")\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/alimTalk/service/SendDoneOrderAlimTalkService.kt",
    "content": "package band.gosrock.api.alimTalk.service\n\nimport band.gosrock.api.alimTalk.dto.OrderAlimTalkDto\nimport band.gosrock.domain.common.alarm.OrderKakaoTalkAlarm\nimport band.gosrock.infrastructure.config.alilmTalk.NcpHelper\nimport org.springframework.stereotype.Service\n\n@Service\nclass SendDoneOrderAlimTalkService(\n    private val ncpHelper: NcpHelper,\n) {\n    fun execute(orderAlimTalkDto: OrderAlimTalkDto) {\n        val userInfo = orderAlimTalkDto.userInfo\n        val eventInfo = orderAlimTalkDto.eventInfo\n        val orderInfo = orderAlimTalkDto.orderInfo\n\n        val content = OrderKakaoTalkAlarm.creationOf(\n            userInfo.userName,\n            eventInfo.hostName,\n            eventInfo.eventName,\n        )\n        val headerContent = OrderKakaoTalkAlarm.creationHeaderOf()\n\n        ncpHelper.sendDoneOrderAlimTalk(\n            userInfo.phoneNum,\n            OrderKakaoTalkAlarm.creationTemplateCode(),\n            content,\n            headerContent,\n            orderInfo,\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/alimTalk/service/SendRegisterAlimTalkService.kt",
    "content": "package band.gosrock.api.alimTalk.service\n\nimport band.gosrock.domain.common.alarm.UserKakaoTalkAlarm\nimport band.gosrock.infrastructure.config.alilmTalk.NcpHelper\nimport org.springframework.stereotype.Service\n\n@Service\nclass SendRegisterAlimTalkService(\n    private val ncpHelper: NcpHelper,\n) {\n    fun execute(userName: String, to: String) {\n        val content = UserKakaoTalkAlarm.creationOf(userName)\n        ncpHelper.sendButtonNcpAlimTalk(to, UserKakaoTalkAlarm.creationTemplateCode(), content)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/alimTalk/service/SendWithdrawOrderAlimTalkService.kt",
    "content": "package band.gosrock.api.alimTalk.service\n\nimport band.gosrock.api.alimTalk.dto.OrderAlimTalkDto\nimport band.gosrock.domain.common.alarm.OrderKakaoTalkAlarm\nimport band.gosrock.infrastructure.config.alilmTalk.NcpHelper\nimport org.springframework.stereotype.Service\n\n@Service\nclass SendWithdrawOrderAlimTalkService(\n    private val ncpHelper: NcpHelper,\n) {\n    fun execute(orderAlimTalkDto: OrderAlimTalkDto) {\n        val userInfo = orderAlimTalkDto.userInfo\n        val eventInfo = orderAlimTalkDto.eventInfo\n        val orderInfo = orderAlimTalkDto.orderInfo\n\n        val content = OrderKakaoTalkAlarm.deletionOf(\n            userInfo.userName,\n            eventInfo.hostName,\n            eventInfo.eventName,\n        )\n        val headerContent = OrderKakaoTalkAlarm.deletionHeaderOf()\n\n        ncpHelper.sendCancelOrderAlimTalk(\n            userInfo.phoneNum,\n            OrderKakaoTalkAlarm.deletionTemplateCode(),\n            content,\n            headerContent,\n            orderInfo,\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/alimTalk/service/helper/OrderAlimTalkInfoHelper.kt",
    "content": "package band.gosrock.api.alimTalk.service.helper\n\nimport band.gosrock.api.alimTalk.dto.OrderAlimTalkDto\nimport band.gosrock.common.annotation.Helper\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.host.domain.Host\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.infrastructure.config.alilmTalk.dto.AlimTalkEventInfo\nimport org.springframework.transaction.annotation.Transactional\n\n@Helper\n@Transactional(readOnly = true)\nclass OrderAlimTalkInfoHelper(\n    private val userAdaptor: UserAdaptor,\n) {\n    fun execute(order: Order, event: Event, host: Host): OrderAlimTalkDto {\n        val user = userAdaptor.queryUser(order.userId)\n        return OrderAlimTalkDto(\n            userInfo = user.toAlimTalkUserInfo(),\n            orderInfo = order.toAlimTalkOrderInfo(),\n            eventInfo = getEventInfo(event, host),\n        )\n    }\n\n    private fun getEventInfo(event: Event, host: Host): AlimTalkEventInfo =\n        AlimTalkEventInfo(host.profile!!.name!!, event.eventBasic!!.name!!)\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/controller/AuthController.kt",
    "content": "package band.gosrock.api.auth.controller\n\nimport band.gosrock.api.auth.model.dto.request.RegisterRequest\nimport band.gosrock.api.auth.model.dto.response.AvailableRegisterResponse\nimport band.gosrock.api.auth.model.dto.response.OauthLoginLinkResponse\nimport band.gosrock.api.auth.model.dto.response.OauthTokenResponse\nimport band.gosrock.api.auth.model.dto.response.OauthUserInfoResponse\nimport band.gosrock.api.auth.model.dto.response.TokenAndUserResponse\nimport band.gosrock.api.auth.service.LocalDevLoginUseCase\nimport band.gosrock.api.auth.service.LoginUseCase\nimport band.gosrock.api.auth.service.LogoutUseCase\nimport band.gosrock.api.auth.service.OauthUserInfoUseCase\nimport band.gosrock.api.auth.service.RefreshUseCase\nimport band.gosrock.api.auth.service.RegisterUseCase\nimport band.gosrock.api.auth.service.WithDrawUseCase\nimport band.gosrock.api.auth.service.helper.CookieHelper\nimport band.gosrock.api.config.rateLimit.UserRateLimiter\nimport band.gosrock.common.annotation.CurrentUserId\nimport band.gosrock.common.annotation.ApiErrorCodeExample\nimport band.gosrock.common.annotation.DevelopOnlyApi\nimport band.gosrock.infrastructure.outer.api.oauth.exception.KakaoKauthErrorCode\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport jakarta.servlet.http.HttpServletRequest\nimport jakarta.validation.Valid\nimport org.slf4j.LoggerFactory\nimport org.springframework.http.ResponseEntity\nimport org.springframework.web.bind.annotation.DeleteMapping\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestBody\nimport org.springframework.web.bind.annotation.RequestHeader\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RequestParam\nimport org.springframework.web.bind.annotation.RestController\n\n@RestController\n@RequestMapping(\"/api/v1/auth\")\n@Tag(name = \"1-1. [인증]\")\nclass AuthController(\n    private val registerUseCase: RegisterUseCase,\n    private val loginUseCase: LoginUseCase,\n    private val refreshUseCase: RefreshUseCase,\n    private val oauthUserInfoUseCase: OauthUserInfoUseCase,\n    private val withDrawUseCase: WithDrawUseCase,\n    private val logoutUseCase: LogoutUseCase,\n    private val cookieHelper: CookieHelper,\n    private val rateLimiter: UserRateLimiter,\n    private val localDevLoginUseCase: LocalDevLoginUseCase\n) {\n    private val log = LoggerFactory.getLogger(AuthController::class.java)\n\n    @Operation(summary = \"kakao oauth 링크발급 (백엔드용 )\", description = \"kakao 링크를 받아볼수 있습니다.\")\n    @Tag(name = \"1-2. [카카오]\")\n    @GetMapping(\"/oauth/kakao/link/test\")\n    fun getKakaoOauthLinkTest(): OauthLoginLinkResponse =\n        registerUseCase.getKaKaoOauthLinkTest()\n\n    @Operation(summary = \"kakao oauth 링크발급 (클라이언트용)\", description = \"kakao 링크를 받아볼수 있습니다.\")\n    @Tag(name = \"1-2. [카카오]\")\n    @GetMapping(\"/oauth/kakao/link\")\n    fun getKakaoOauthLink(\n        @RequestHeader(value = \"referer\", required = false) referer: String,\n        @RequestHeader(value = \"host\", required = false) host: String\n    ): OauthLoginLinkResponse {\n        // 스테이징, prod 서버에 배포된 클라이언트에 해당\n        if (referer.contains(host)) {\n            log.info(\"/oauth/kakao$host\")\n            val format = \"https://$host\"\n            if (referer.contains(\"admin\")) {\n                return registerUseCase.getKaKaoOauthLink(\"$format/admin\")\n            }\n            return registerUseCase.getKaKaoOauthLink(format)\n        } else if (referer.contains(\"5173\")) {\n            return registerUseCase.getKaKaoOauthLink(\"$referer/admin\")\n        }\n        return registerUseCase.getKaKaoOauthLink(referer)\n    }\n\n    @Operation(summary = \"카카오 code 요청받는 곳입니다. referer,host는 건들이지 말아주세요!안보내셔도됩니다.\")\n    @Tag(name = \"1-2. [카카오]\")\n    @GetMapping(\"/oauth/kakao\")\n    @ApiErrorCodeExample(KakaoKauthErrorCode::class)\n    fun getCredentialFromKaKao(\n        @RequestParam(\"code\") code: String,\n        @RequestHeader(value = \"referer\", required = false) referer: String,\n        @RequestHeader(value = \"host\", required = false) host: String\n    ): OauthTokenResponse {\n        if (referer.contains(host)) {\n            log.info(\"/oauth/kakao$host\")\n            val format = \"https://$host\"\n            if (referer.contains(\"admin\")) {\n                return registerUseCase.getCredentialFromKaKao(code, \"$format/admin\")\n            }\n            return registerUseCase.getCredentialFromKaKao(code, format)\n        } else if (referer.contains(\"5173\")) {\n            return registerUseCase.getCredentialFromKaKao(code, \"$referer/admin\")\n        }\n        return registerUseCase.getCredentialFromKaKao(code, referer)\n    }\n\n    @Operation(summary = \"개발용 회원가입입니다 클라이언트가 몰라도 됩니다.\", deprecated = true)\n    @Tag(name = \"1-2. [카카오]\")\n    @DevelopOnlyApi\n    @GetMapping(\"/oauth/kakao/develop\")\n    fun developUserSign(@RequestParam(\"code\") code: String): ResponseEntity<TokenAndUserResponse> {\n        val tokenAndUserResponse = registerUseCase.upsertKakaoOauthUser(code)\n        return ResponseEntity.ok()\n            .headers(cookieHelper.getTokenCookies(tokenAndUserResponse))\n            .body(tokenAndUserResponse)\n    }\n\n    @Operation(summary = \"회원가입이 가능한지 id token 으로 확인합니다.\")\n    @Tag(name = \"1-2. [카카오]\")\n    @GetMapping(\"/oauth/kakao/register/valid\")\n    fun kakaoAuthCheckRegisterValid(\n        @RequestParam(\"id_token\") token: String\n    ): AvailableRegisterResponse =\n        registerUseCase.checkAvailableRegister(token)\n\n    @Operation(summary = \"id_token 으로 회원가입을 합니다.\")\n    @Tag(name = \"1-2. [카카오]\")\n    @PostMapping(\"/oauth/kakao/register\")\n    fun kakaoAuthRegister(\n        @RequestParam(\"id_token\") token: String,\n        @Valid @RequestBody registerRequest: RegisterRequest\n    ): ResponseEntity<TokenAndUserResponse> {\n        val tokenAndUserResponse = registerUseCase.registerUserByOCIDToken(token, registerRequest)\n        return ResponseEntity.ok()\n            .headers(cookieHelper.getTokenCookies(tokenAndUserResponse))\n            .body(tokenAndUserResponse)\n    }\n\n    @Operation(summary = \"id_token 으로 로그인을 합니다.\")\n    @Tag(name = \"1-2. [카카오]\")\n    @PostMapping(\"/oauth/kakao/login\")\n    fun kakaoOauthUserLogin(\n        @RequestParam(\"id_token\") token: String\n    ): ResponseEntity<TokenAndUserResponse> {\n        val tokenAndUserResponse = loginUseCase.execute(token)\n        return ResponseEntity.ok()\n            .headers(cookieHelper.getTokenCookies(tokenAndUserResponse))\n            .body(tokenAndUserResponse)\n    }\n\n    @Operation(summary = \"accessToken 으로 oauth user 정보를 가져옵니다.\")\n    @Tag(name = \"1-2. [카카오]\")\n    @PostMapping(\"/oauth/kakao/info\")\n    fun kakaoOauthUserInfo(\n        @RequestParam(\"access_token\") accessToken: String\n    ): OauthUserInfoResponse =\n        oauthUserInfoUseCase.execute(accessToken)\n\n    @Operation(summary = \"refreshToken 용입니다.\")\n    @PostMapping(\"/token/refresh\")\n    fun tokenRefresh(\n        request: HttpServletRequest,\n        @RequestParam(value = \"token\", required = false, defaultValue = \"\") refreshToken: String\n    ): ResponseEntity<TokenAndUserResponse> {\n        val refreshTokenCookie = cookieHelper.getRefreshTokenFromRequest(request)\n        val tokenAndUserResponse = refreshUseCase.execute(refreshTokenCookie ?: refreshToken)\n        return ResponseEntity.ok()\n            .headers(cookieHelper.getTokenCookies(tokenAndUserResponse))\n            .body(tokenAndUserResponse)\n    }\n\n    @Operation(summary = \"회원탈퇴를 합니다.\")\n    @SecurityRequirement(name = \"access-token\")\n    @DeleteMapping(\"/me\")\n    fun withDrawUser(@CurrentUserId userId: Long): ResponseEntity<Void> {\n        withDrawUseCase.execute(userId)\n        return ResponseEntity.ok().headers(cookieHelper.deleteCookies()).body(null)\n    }\n\n    @Operation(summary = \"로그아웃을 합니다.\")\n    @SecurityRequirement(name = \"access-token\")\n    @PostMapping(\"/logout\")\n    fun logoutUser(@CurrentUserId userId: Long): ResponseEntity<Void> {\n        logoutUseCase.execute(userId)\n        return ResponseEntity.ok().headers(cookieHelper.deleteCookies()).body(null)\n    }\n\n    @Operation(summary = \"로컬 개발용 즉시 로그인 (카카오 불필요)\", deprecated = true)\n    @Tag(name = \"1-2. [카카오]\")\n    @DevelopOnlyApi\n    @PostMapping(\"/oauth/local/login\")\n    fun localDevLogin(\n        @Valid @RequestBody registerRequest: RegisterRequest\n    ): ResponseEntity<TokenAndUserResponse> {\n        val tokenAndUserResponse = localDevLoginUseCase.execute(registerRequest)\n        return ResponseEntity.ok()\n            .headers(cookieHelper.getTokenCookies(tokenAndUserResponse))\n            .body(tokenAndUserResponse)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/model/dto/KakaoUserInfoDto.kt",
    "content": "package band.gosrock.api.auth.model.dto\n\nimport band.gosrock.domain.domains.user.domain.OauthInfo\nimport band.gosrock.domain.domains.user.domain.OauthProvider\nimport band.gosrock.domain.domains.user.domain.Profile\n\ndata class KakaoUserInfoDto(\n    val oauthId: String,\n    val email: String?,\n    val phoneNumber: String?,\n    val profileImage: String?,\n    val name: String?,\n    val oauthProvider: OauthProvider\n) {\n    fun toProfile(): Profile = Profile(\n        profileImage = profileImage,\n        phoneNumber = phoneNumber,\n        name = name ?: \"\",\n        email = email ?: \"\",\n    )\n\n    fun toOauthInfo(): OauthInfo = OauthInfo(\n        oid = oauthId,\n        provider = oauthProvider,\n    )\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/model/dto/request/RegisterRequest.kt",
    "content": "package band.gosrock.api.auth.model.dto.request\n\nimport band.gosrock.domain.domains.user.domain.Profile\nimport jakarta.validation.constraints.NotEmpty\n\ndata class RegisterRequest(\n    @field:NotEmpty\n    val email: String? = null,\n    val phoneNumber: String? = null,\n    val profileImage: String? = null,\n    @field:NotEmpty\n    val name: String? = null,\n    val marketingAgree: Boolean = false\n) {\n    fun toProfile(): Profile = Profile(\n        profileImage = profileImage,\n        phoneNumber = phoneNumber,\n        name = name ?: \"\",\n        email = email ?: \"\",\n    )\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/model/dto/response/AvailableRegisterResponse.kt",
    "content": "package band.gosrock.api.auth.model.dto.response\n\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class AvailableRegisterResponse(\n    @Schema(description = \"회원가입을 했던 유저인지에 대한 여부 , oauth 요청을 통해 처음 회원가입한경우 false임\")\n    val canRegister: Boolean\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/model/dto/response/OauthLoginLinkResponse.kt",
    "content": "package band.gosrock.api.auth.model.dto.response\n\ndata class OauthLoginLinkResponse(\n    val link: String\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/model/dto/response/OauthTokenResponse.kt",
    "content": "package band.gosrock.api.auth.model.dto.response\n\nimport band.gosrock.infrastructure.outer.api.oauth.dto.KakaoTokenResponse\n\ndata class OauthTokenResponse(\n    val accessToken: String?,\n    val refreshToken: String?,\n    val idToken: String?\n) {\n    companion object {\n        fun from(kakaoTokenResponse: KakaoTokenResponse): OauthTokenResponse =\n            OauthTokenResponse(\n                idToken = kakaoTokenResponse.idToken,\n                refreshToken = kakaoTokenResponse.refreshToken,\n                accessToken = kakaoTokenResponse.accessToken\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/model/dto/response/OauthUserInfoResponse.kt",
    "content": "package band.gosrock.api.auth.model.dto.response\n\nimport band.gosrock.api.auth.model.dto.KakaoUserInfoDto\n\ndata class OauthUserInfoResponse(\n    val email: String?,\n    val phoneNumber: String?,\n    val profileImage: String?,\n    val name: String?\n) {\n    companion object {\n        fun from(kakaoUserInfoDto: KakaoUserInfoDto): OauthUserInfoResponse =\n            OauthUserInfoResponse(\n                email = kakaoUserInfoDto.email,\n                phoneNumber = kakaoUserInfoDto.phoneNumber,\n                profileImage = kakaoUserInfoDto.profileImage,\n                name = kakaoUserInfoDto.name\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/model/dto/response/TokenAndUserResponse.kt",
    "content": "package band.gosrock.api.auth.model.dto.response\n\nimport band.gosrock.domain.common.dto.ProfileViewDto\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class TokenAndUserResponse(\n    @Schema(description = \"어세스 토큰\")\n    val accessToken: String,\n    val accessTokenAge: Long,\n    @Schema(description = \"리프레쉬 토큰\")\n    val refreshToken: String,\n    val refreshTokenAge: Long,\n    val userProfile: ProfileViewDto\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/service/LocalDevLoginUseCase.kt",
    "content": "package band.gosrock.api.auth.service\n\nimport band.gosrock.api.auth.model.dto.request.RegisterRequest\nimport band.gosrock.api.auth.model.dto.response.TokenAndUserResponse\nimport band.gosrock.api.auth.service.helper.TokenGenerateHelper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.user.domain.OauthInfo\nimport band.gosrock.domain.domains.user.domain.OauthProvider\nimport band.gosrock.domain.domains.user.service.UserDomainService\n\n@UseCase\nclass LocalDevLoginUseCase(\n    private val userDomainService: UserDomainService,\n    private val tokenGenerateHelper: TokenGenerateHelper\n) {\n    fun execute(registerRequest: RegisterRequest): TokenAndUserResponse {\n        val oauthInfo = OauthInfo(\n            provider = OauthProvider.KAKAO,\n            oid = registerRequest.email ?: \"anonymous\",\n        )\n\n        val profile = registerRequest.toProfile()\n        val user = userDomainService.upsertUser(profile, oauthInfo)\n        return tokenGenerateHelper.execute(user)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/service/LoginUseCase.kt",
    "content": "package band.gosrock.api.auth.service\n\nimport band.gosrock.api.auth.model.dto.response.TokenAndUserResponse\nimport band.gosrock.api.auth.service.helper.KakaoOauthHelper\nimport band.gosrock.api.auth.service.helper.TokenGenerateHelper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.user.service.UserDomainService\nimport org.slf4j.LoggerFactory\n\n@UseCase\nclass LoginUseCase(\n    private val kakaoOauthHelper: KakaoOauthHelper,\n    private val userDomainService: UserDomainService,\n    private val tokenGenerateHelper: TokenGenerateHelper\n) {\n    private val log = LoggerFactory.getLogger(LoginUseCase::class.java)\n\n    fun execute(idToken: String): TokenAndUserResponse {\n        log.info(\"[LoginUseCase][execute] 로그인 시도\")\n        val oauthInfo = kakaoOauthHelper.getOauthInfoByIdToken(idToken)\n        val user = userDomainService.loginUser(oauthInfo)\n        log.info(\"[LoginUseCase][execute] 로그인 성공 userId={}\", user.id)\n        return tokenGenerateHelper.execute(user)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/service/LogoutUseCase.kt",
    "content": "package band.gosrock.api.auth.service\n\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.user.adaptor.RefreshTokenAdaptor\n\n@UseCase\nclass LogoutUseCase(\n    private val refreshTokenAdaptor: RefreshTokenAdaptor\n) {\n\n    fun execute(userId: Long) {\n        refreshTokenAdaptor.deleteByUserId(userId)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/service/OauthUserInfoUseCase.kt",
    "content": "package band.gosrock.api.auth.service\n\nimport band.gosrock.api.auth.model.dto.response.OauthUserInfoResponse\nimport band.gosrock.api.auth.service.helper.KakaoOauthHelper\nimport band.gosrock.common.annotation.UseCase\n\n@UseCase\nclass OauthUserInfoUseCase(\n    private val kakaoOauthHelper: KakaoOauthHelper\n) {\n\n    fun execute(accessToken: String): OauthUserInfoResponse {\n        val oauthUserInfo = kakaoOauthHelper.getUserInfo(accessToken)\n        return OauthUserInfoResponse.from(oauthUserInfo)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/service/RefreshUseCase.kt",
    "content": "package band.gosrock.api.auth.service\n\nimport band.gosrock.api.auth.model.dto.response.TokenAndUserResponse\nimport band.gosrock.api.auth.service.helper.TokenGenerateHelper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.common.jwt.JwtTokenProvider\nimport band.gosrock.domain.domains.user.adaptor.RefreshTokenAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.domain.domains.user.service.UserDomainService\nimport org.slf4j.LoggerFactory\n\n@UseCase\nclass RefreshUseCase(\n    private val userAdaptor: UserAdaptor,\n    private val jwtTokenProvider: JwtTokenProvider,\n    private val userDomainService: UserDomainService,\n    private val refreshTokenAdaptor: RefreshTokenAdaptor,\n    private val tokenGenerateHelper: TokenGenerateHelper\n) {\n    private val log = LoggerFactory.getLogger(RefreshUseCase::class.java)\n\n    fun execute(refreshToken: String): TokenAndUserResponse {\n        val savedRefreshTokenEntity = refreshTokenAdaptor.queryRefreshToken(refreshToken)\n        val refreshUserId = jwtTokenProvider.parseRefreshToken(savedRefreshTokenEntity.refreshToken!!)\n        log.info(\"[RefreshUseCase][execute] 토큰 갱신 userId={}\", refreshUserId)\n        val user = userAdaptor.queryUser(refreshUserId)\n        // 리프레쉬 시에도 last로그인 정보 업데이트\n        userDomainService.loginUser(user.oauthInfo!!)\n        return tokenGenerateHelper.execute(user)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/service/RegisterUseCase.kt",
    "content": "package band.gosrock.api.auth.service\n\nimport band.gosrock.api.auth.model.dto.request.RegisterRequest\nimport band.gosrock.api.auth.model.dto.response.AvailableRegisterResponse\nimport band.gosrock.api.auth.model.dto.response.OauthLoginLinkResponse\nimport band.gosrock.api.auth.model.dto.response.OauthTokenResponse\nimport band.gosrock.api.auth.model.dto.response.TokenAndUserResponse\nimport band.gosrock.api.auth.service.helper.KakaoOauthHelper\nimport band.gosrock.api.auth.service.helper.TokenGenerateHelper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.user.service.UserDomainService\nimport org.slf4j.LoggerFactory\n\n@UseCase\nclass RegisterUseCase(\n    private val kakaoOauthHelper: KakaoOauthHelper,\n    private val userDomainService: UserDomainService,\n    private val tokenGenerateHelper: TokenGenerateHelper\n) {\n    private val log = LoggerFactory.getLogger(RegisterUseCase::class.java)\n\n    fun getKaKaoOauthLinkTest(): OauthLoginLinkResponse =\n        OauthLoginLinkResponse(kakaoOauthHelper.getKaKaoOauthLinkTest())\n\n    fun getKaKaoOauthLink(referer: String): OauthLoginLinkResponse =\n        OauthLoginLinkResponse(kakaoOauthHelper.getKaKaoOauthLink(referer))\n\n    fun upsertKakaoOauthUser(code: String): TokenAndUserResponse {\n        val oauthAccessToken = kakaoOauthHelper.getOauthTokenTest(code).accessToken\n        val oauthUserInfo = kakaoOauthHelper.getUserInfo(oauthAccessToken!!)\n\n        val profile = oauthUserInfo.toProfile()\n        val user = userDomainService.upsertUser(profile, oauthUserInfo.toOauthInfo())\n\n        return tokenGenerateHelper.execute(user)\n    }\n\n    fun checkAvailableRegister(idToken: String): AvailableRegisterResponse {\n        val oauthInfo = kakaoOauthHelper.getOauthInfoByIdToken(idToken)\n        return AvailableRegisterResponse(userDomainService.checkUserCanRegister(oauthInfo))\n    }\n\n    fun registerUserByOCIDToken(\n        idToken: String,\n        registerUserRequest: RegisterRequest\n    ): TokenAndUserResponse {\n        log.info(\"[RegisterUseCase][registerUserByOCIDToken] 회원가입\")\n        val oauthInfo = kakaoOauthHelper.getOauthInfoByIdToken(idToken)\n        val user = userDomainService.registerUser(\n            registerUserRequest.toProfile(),\n            oauthInfo,\n            registerUserRequest.marketingAgree\n        )\n        log.info(\"[RegisterUseCase][registerUserByOCIDToken] 회원가입 완료 userId={}\", user.id)\n        return tokenGenerateHelper.execute(user)\n    }\n\n    fun getCredentialFromKaKao(code: String, referer: String): OauthTokenResponse =\n        OauthTokenResponse.from(kakaoOauthHelper.getOauthToken(code, referer))\n\n    fun getCredentialFromKaKaoTest(code: String): OauthTokenResponse =\n        OauthTokenResponse.from(kakaoOauthHelper.getOauthTokenTest(code))\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/service/WithDrawUseCase.kt",
    "content": "package band.gosrock.api.auth.service\n\nimport band.gosrock.api.auth.service.helper.KakaoOauthHelper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.user.adaptor.RefreshTokenAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.domain.domains.user.service.UserDomainService\nimport org.slf4j.LoggerFactory\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass WithDrawUseCase(\n    private val refreshTokenAdaptor: RefreshTokenAdaptor,\n    private val userDomainService: UserDomainService,\n    private val userAdaptor: UserAdaptor,\n    private val kakaoOauthHelper: KakaoOauthHelper\n) {\n    private val log = LoggerFactory.getLogger(WithDrawUseCase::class.java)\n\n    @Transactional\n    fun execute(userId: Long) {\n        log.info(\"[WithDrawUseCase][execute] 회원 탈퇴 userId={}\", userId)\n        refreshTokenAdaptor.deleteByUserId(userId)\n        val user = userAdaptor.queryUser(userId)\n        val oid = user.oauthInfo!!.oid!!\n        userDomainService.withDrawUser(userId)\n        kakaoOauthHelper.unlink(oid)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/service/helper/CookieHelper.kt",
    "content": "package band.gosrock.api.auth.service.helper\n\nimport band.gosrock.api.auth.model.dto.response.TokenAndUserResponse\nimport band.gosrock.common.annotation.Helper\nimport band.gosrock.common.helper.SpringEnvironmentHelper\nimport jakarta.servlet.http.HttpServletRequest\nimport org.springframework.http.HttpHeaders\nimport org.springframework.http.ResponseCookie\n\n@Helper\nclass CookieHelper(\n    private val springEnvironmentHelper: SpringEnvironmentHelper\n) {\n\n    fun getAccessTokenName(): String = \"accessToken\"\n\n    fun getRefreshTokenName(): String = \"refreshToken\"\n\n    fun getRefreshTokenFromRequest(request: HttpServletRequest): String? =\n        request.cookies?.firstOrNull { it.name == getRefreshTokenName() }?.value\n\n    fun getTokenCookies(tokenAndUserResponse: TokenAndUserResponse): HttpHeaders {\n        val accessToken = buildCookie(\n            getAccessTokenName(),\n            tokenAndUserResponse.accessToken,\n            tokenAndUserResponse.accessTokenAge\n        )\n        val refreshToken = buildCookie(\n            getRefreshTokenName(),\n            tokenAndUserResponse.refreshToken,\n            tokenAndUserResponse.refreshTokenAge\n        )\n\n        return HttpHeaders().apply {\n            add(HttpHeaders.SET_COOKIE, accessToken.toString())\n            add(HttpHeaders.SET_COOKIE, refreshToken.toString())\n        }\n    }\n\n    fun deleteCookies(): HttpHeaders {\n        val accessToken = buildCookie(getAccessTokenName(), \"\", 0)\n        val refreshToken = buildCookie(getRefreshTokenName(), \"\", 0)\n\n        return HttpHeaders().apply {\n            add(HttpHeaders.SET_COOKIE, accessToken.toString())\n            add(HttpHeaders.SET_COOKIE, refreshToken.toString())\n        }\n    }\n\n    private fun buildCookie(name: String, value: String, maxAge: Long): ResponseCookie {\n        val isProdOrStaging = springEnvironmentHelper.isProdAndStagingProfile()\n        val sameSite = if (springEnvironmentHelper.isProdProfile()) \"Strict\" else \"None\"\n\n        return ResponseCookie.from(name, value)\n            .path(\"/\")\n            .maxAge(maxAge)\n            .sameSite(sameSite)\n            .secure(true)\n            .apply {\n                if (isProdOrStaging) {\n                    domain(\".dudoong.com\")\n                }\n            }\n            .build()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/service/helper/KakaoOauthHelper.kt",
    "content": "package band.gosrock.api.auth.service.helper\n\nimport band.gosrock.api.auth.model.dto.KakaoUserInfoDto\nimport band.gosrock.common.annotation.Helper\nimport band.gosrock.common.consts.DuDoongStatic.BEARER\nimport band.gosrock.common.consts.DuDoongStatic.KAKAO_OAUTH_QUERY_STRING\nimport band.gosrock.common.dto.OIDCDecodePayload\nimport band.gosrock.common.properties.OauthProperties\nimport band.gosrock.domain.domains.user.domain.OauthInfo\nimport band.gosrock.domain.domains.user.domain.OauthProvider\nimport band.gosrock.infrastructure.outer.api.oauth.client.KakaoInfoClient\nimport band.gosrock.infrastructure.outer.api.oauth.client.KakaoOauthClient\nimport band.gosrock.infrastructure.outer.api.oauth.dto.KakaoTokenResponse\nimport band.gosrock.infrastructure.outer.api.oauth.dto.UnlinkKaKaoTarget\n\n@Helper\nclass KakaoOauthHelper(\n    private val oauthProperties: OauthProperties,\n    private val kakaoInfoClient: KakaoInfoClient,\n    private val kakaoOauthClient: KakaoOauthClient,\n    private val oauthOIDCHelper: OauthOIDCHelper\n) {\n\n    fun getKaKaoOauthLinkTest(): String =\n        oauthProperties.getKakaoBaseUrl() + String.format(\n            KAKAO_OAUTH_QUERY_STRING,\n            oauthProperties.getKakaoClientId(),\n            oauthProperties.getKakaoRedirectUrl()\n        )\n\n    fun getKaKaoOauthLink(referer: String): String =\n        oauthProperties.getKakaoBaseUrl() + String.format(\n            KAKAO_OAUTH_QUERY_STRING,\n            oauthProperties.getKakaoClientId(),\n            \"$referer/kakao/callback\"\n        )\n\n    fun getOauthToken(code: String, referer: String): KakaoTokenResponse =\n        kakaoOauthClient.kakaoAuth(\n            oauthProperties.getKakaoClientId(),\n            \"$referer/kakao/callback\",\n            code,\n            oauthProperties.getKakaoClientSecret()\n        )\n\n    fun getOauthTokenTest(code: String): KakaoTokenResponse =\n        kakaoOauthClient.kakaoAuth(\n            oauthProperties.getKakaoClientId(),\n            oauthProperties.getKakaoRedirectUrl(),\n            code,\n            oauthProperties.getKakaoClientSecret()\n        )\n\n    fun getUserInfo(oauthAccessToken: String): KakaoUserInfoDto {\n        val response = kakaoInfoClient.kakaoUserInfo(BEARER + oauthAccessToken)\n\n        return KakaoUserInfoDto(\n            oauthProvider = OauthProvider.KAKAO,\n            name = response.getName(),\n            phoneNumber = response.getPhoneNumber(),\n            profileImage = response.getProfileUrl(),\n            email = response.getEmail(),\n            oauthId = response.id ?: \"\"\n        )\n    }\n\n    fun getOIDCDecodePayload(token: String): OIDCDecodePayload {\n        val oidcPublicKeysResponse = kakaoOauthClient.getKakaoOIDCOpenKeys()\n        return oauthOIDCHelper.getPayloadFromIdToken(\n            token,\n            oauthProperties.getKakaoBaseUrl(),\n            oauthProperties.getKakaoAppId(),\n            oidcPublicKeysResponse\n        )\n    }\n\n    fun getOauthInfoByIdToken(idToken: String): OauthInfo {\n        val oidcDecodePayload = getOIDCDecodePayload(idToken)\n        return OauthInfo(\n            provider = OauthProvider.KAKAO,\n            oid = oidcDecodePayload.sub,\n        )\n    }\n\n    fun unlink(oid: String) {\n        val kakaoAdminKey = oauthProperties.getKakaoAdminKey()\n        val unlinkKaKaoTarget = UnlinkKaKaoTarget.from(oid)\n        val header = \"KakaoAK $kakaoAdminKey\"\n        kakaoInfoClient.unlinkUser(header, unlinkKaKaoTarget)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/service/helper/OauthOIDCHelper.kt",
    "content": "package band.gosrock.api.auth.service.helper\n\nimport band.gosrock.common.annotation.Helper\nimport band.gosrock.common.dto.OIDCDecodePayload\nimport band.gosrock.common.jwt.JwtOIDCProvider\nimport band.gosrock.infrastructure.outer.api.oauth.dto.OIDCPublicKeysResponse\n\n@Helper\nclass OauthOIDCHelper(\n    private val jwtOIDCProvider: JwtOIDCProvider\n) {\n\n    private fun getKidFromUnsignedIdToken(token: String, iss: String, aud: String): String =\n        jwtOIDCProvider.getKidFromUnsignedTokenHeader(token, iss, aud)\n\n    fun getPayloadFromIdToken(\n        token: String,\n        iss: String,\n        aud: String,\n        oidcPublicKeysResponse: OIDCPublicKeysResponse\n    ): OIDCDecodePayload {\n        val kid = getKidFromUnsignedIdToken(token, iss, aud)\n\n        val oidcPublicKeyDto = oidcPublicKeysResponse.keys!!.first { it.kid == kid }\n\n        return jwtOIDCProvider.getOIDCTokenBody(token, oidcPublicKeyDto.n!!, oidcPublicKeyDto.e!!)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/auth/service/helper/TokenGenerateHelper.kt",
    "content": "package band.gosrock.api.auth.service.helper\n\nimport band.gosrock.api.auth.model.dto.response.TokenAndUserResponse\nimport band.gosrock.common.annotation.Helper\nimport band.gosrock.common.jwt.JwtTokenProvider\nimport band.gosrock.domain.common.dto.ProfileViewDto\nimport band.gosrock.domain.domains.user.adaptor.RefreshTokenAdaptor\nimport band.gosrock.domain.domains.user.domain.RefreshTokenEntity\nimport band.gosrock.domain.domains.user.domain.User\nimport org.springframework.transaction.annotation.Transactional\n\n@Helper\nclass TokenGenerateHelper(\n    private val jwtTokenProvider: JwtTokenProvider,\n    private val refreshTokenAdaptor: RefreshTokenAdaptor\n) {\n\n    @Transactional\n    fun execute(user: User): TokenAndUserResponse =\n        generateTokenResponse(user, admin = false)\n\n    @Transactional\n    fun executeAdmin(user: User): TokenAndUserResponse =\n        generateTokenResponse(user, admin = true)\n\n    private fun generateTokenResponse(user: User, admin: Boolean): TokenAndUserResponse {\n        val userId = user.id!!\n        val newAccessToken = if (admin) {\n            jwtTokenProvider.generateAdminAccessToken(userId)\n        } else {\n            jwtTokenProvider.generateAccessToken(userId)\n        }\n        val newRefreshToken = jwtTokenProvider.generateRefreshToken(userId)\n\n        val newRefreshTokenEntity = RefreshTokenEntity(\n            refreshToken = newRefreshToken,\n            id = userId,\n            ttl = jwtTokenProvider.getRefreshTokenTTlSecond(),\n        )\n        refreshTokenAdaptor.save(newRefreshTokenEntity)\n\n        return TokenAndUserResponse(\n            userProfile = ProfileViewDto.from(user),\n            accessToken = newAccessToken,\n            accessTokenAge = jwtTokenProvider.getAccessTokenTTlSecond(),\n            refreshTokenAge = jwtTokenProvider.getRefreshTokenTTlSecond(),\n            refreshToken = newRefreshToken\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/cart/controller/CartController.kt",
    "content": "package band.gosrock.api.cart.controller\n\nimport band.gosrock.api.cart.docs.CreateCartExceptionDocs\nimport band.gosrock.api.cart.model.dto.request.AddCartRequest\nimport band.gosrock.api.cart.model.dto.response.CartResponse\nimport band.gosrock.api.cart.service.CreateCartUseCase\nimport band.gosrock.api.cart.service.ReadCartUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport band.gosrock.common.annotation.ApiErrorExceptionsExample\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport jakarta.validation.Valid\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestBody\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RestController\n\n@SecurityRequirement(name = \"access-token\")\n@Tag(name = \"5. [장바구니]\")\n@RestController\n@RequestMapping(\"/api/v1/carts\")\nclass CartController(\n    private val createCartUseCase: CreateCartUseCase,\n    private val readCartUseCase: ReadCartUseCase,\n) {\n    @Operation(summary = \"상품을 장바구니에 담습니다. 상품에 답변해야하는 응답이 있다면, 응답도 보내주시면 됩니다.\")\n    @ApiErrorExceptionsExample(CreateCartExceptionDocs::class)\n    @PostMapping\n    fun createCartLines(@CurrentUserId userId: Long, @RequestBody @Valid addCartRequest: AddCartRequest): CartResponse {\n        return createCartUseCase.execute(userId, addCartRequest)\n    }\n\n    @Operation(summary = \"사용자가 최근에 만들었던 장바구니를 불러옵니다. 없으면 data null (구현 안해도 됨)\")\n    @GetMapping(\"/recent\")\n    fun getRecentMyCart(@CurrentUserId userId: Long): CartResponse? {\n        return readCartUseCase.execute(userId)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/cart/docs/CreateCartExceptionDocs.kt",
    "content": "package band.gosrock.api.cart.docs\n\nimport band.gosrock.common.annotation.ExceptionDoc\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.exception.DuDoongCodeException\nimport band.gosrock.common.interfaces.SwaggerExampleExceptions\nimport band.gosrock.domain.domains.cart.exception.CartInvalidOptionAnswerException\nimport band.gosrock.domain.domains.cart.exception.CartItemNotOneTypeException\nimport band.gosrock.domain.domains.cart.exception.CartNotAnswerAllOptionGroupException\nimport band.gosrock.domain.domains.event.exception.EventNotOpenException\nimport band.gosrock.domain.domains.event.exception.EventTicketingTimeIsPassedException\nimport band.gosrock.domain.domains.ticket_item.exception.TicketItemQuantityLackException\nimport band.gosrock.domain.domains.ticket_item.exception.TicketPurchaseLimitException\n\n@ExceptionDoc\nclass CreateCartExceptionDocs : SwaggerExampleExceptions {\n\n    @ExplainError(\"아이템에 대한 옵션에 대한 응답을 올바르게 안했을때 ( 한 옵션그룹에 한 응답, 옵션그룹에 응답안했을때)\")\n    val 응답_올바르게_안했을_때: DuDoongCodeException = CartInvalidOptionAnswerException.EXCEPTION\n\n    @ExplainError(\"모든 질문지에 대답을 안했을 때\")\n    val 응답_다대답_안했을때: DuDoongCodeException = CartNotAnswerAllOptionGroupException.EXCEPTION\n\n    @ExplainError(\"이벤트가 열린 상태가 아닐때\")\n    val 이벤트_닫힘: DuDoongCodeException = EventNotOpenException.EXCEPTION\n\n    @ExplainError(\"이벤트 티켓팅 시간이 지났을때.\")\n    val 티켓팅_시간지남: DuDoongCodeException = EventTicketingTimeIsPassedException.EXCEPTION\n\n    @ExplainError(\"티켓 아이템이 한 종류가 아닐 떄\")\n    val 아이템은_한종류여야함: DuDoongCodeException = CartItemNotOneTypeException.EXCEPTION\n\n    @ExplainError(\"아이템의 재고가 부족한 상태일 때\")\n    val 티켓팅_재고부족: DuDoongCodeException = TicketItemQuantityLackException.EXCEPTION\n\n    @ExplainError(\"티켓당 1인당 구매갯수 제한을 넘었을때\")\n    val 티켓팅_구매갯수제한: DuDoongCodeException = TicketPurchaseLimitException.EXCEPTION\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/cart/model/dto/request/AddCartLineDto.kt",
    "content": "package band.gosrock.api.cart.model.dto.request\n\nimport io.swagger.v3.oas.annotations.media.Schema\nimport jakarta.validation.constraints.Min\n\ndata class AddCartLineDto(\n    @Schema(description = \"주문할 아이템 아이디\", defaultValue = \"1\")\n    val itemId: Long,\n\n    @Schema(description = \"상품 수량\", defaultValue = \"1\")\n    @field:Min(1)\n    val quantity: Long,\n\n    @Schema(description = \"상품 관련 옵션에 대한 답변\")\n    val options: List<AddCartOptionAnswerDto>,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/cart/model/dto/request/AddCartOptionAnswerDto.kt",
    "content": "package band.gosrock.api.cart.model.dto.request\n\nimport io.swagger.v3.oas.annotations.media.Schema\nimport jakarta.validation.constraints.NotBlank\nimport jakarta.validation.constraints.NotNull\n\ndata class AddCartOptionAnswerDto(\n    @Schema(description = \"옵션 아이디\")\n    @field:NotNull\n    val optionId: Long,\n\n    @Schema(description = \"옵션 그룹에 대한 응답/ T/F면 예,아니오 ,서술형이면 서술응답\", defaultValue = \"예\")\n    @field:NotBlank\n    val answer: String,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/cart/model/dto/request/AddCartRequest.kt",
    "content": "package band.gosrock.api.cart.model.dto.request\n\nimport io.swagger.v3.oas.annotations.media.Schema\nimport jakarta.validation.constraints.Size\n\ndata class AddCartRequest(\n    @Schema(description = \"상품에 옵션이 있을시에 각기 답변마다 여러개를 보내주시면됩니다. 한번에 답변하기면 하나에 quantity 를 늘리면 됩니다.\")\n    @field:Size(min = 1)\n    val items: List<AddCartLineDto>,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/cart/model/dto/response/CartItemResponse.kt",
    "content": "package band.gosrock.api.cart.model.dto.response\n\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.common.vo.OptionAnswerVo\nimport band.gosrock.domain.domains.cart.domain.CartLineItem\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class CartItemResponse(\n    @Schema(description = \"카트라인의 이름입니다.\", defaultValue = \"일반티켓 2매\")\n    val name: String,\n\n    val answers: List<OptionAnswerVo>,\n\n    @Schema(description = \"아이템 공급가액입니다.\", defaultValue = \"3000원\")\n    val itemPrice: Money,\n\n    @Schema(\n        description = \"카트 라인의 총 가격입니다. 옵션등을 통해서 공급가액에 합산되는 형식입니다. (아이템가격 + 옵션가) * 아이템 개수\",\n        defaultValue = \"4000원\",\n    )\n    val cartLinePrice: Money,\n\n    @Schema(description = \"담은 상품의 개수입니다.\", defaultValue = \"1\")\n    val packedQuantity: Long,\n\n    @Schema(description = \"각 옵션 가격\")\n    val eachOptionPrice: Money,\n) {\n    companion object {\n        @JvmStatic\n        fun of(cartLineItem: CartLineItem, itemName: String, answers: List<OptionAnswerVo>): CartItemResponse {\n            return CartItemResponse(\n                answers = answers,\n                name = itemName,\n                cartLinePrice = cartLineItem.getTotalCartLinePrice(),\n                itemPrice = cartLineItem.itemPrice!!,\n                packedQuantity = cartLineItem.quantity!!,\n                eachOptionPrice = cartLineItem.getTotalOptionsPrice(),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/cart/model/dto/response/CartResponse.kt",
    "content": "package band.gosrock.api.cart.model.dto.response\n\nimport band.gosrock.domain.common.vo.AccountInfoVo\nimport band.gosrock.domain.common.vo.EventProfileVo\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.cart.domain.Cart\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport band.gosrock.domain.domains.ticket_item.domain.TicketPayType\nimport band.gosrock.domain.domains.ticket_item.domain.TicketType\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class CartResponse(\n    @Schema(description = \"장바구니명 입니다.\", defaultValue = \"\")\n    val title: String,\n\n    val items: List<CartItemResponse>,\n\n    @Schema(description = \"카트라인들의 총 결제금액을 합한 금액입니다\", defaultValue = \"15000원\")\n    val totalPrice: Money,\n\n    @Schema(description = \"생성한 장바구니의 아이디입니다\", defaultValue = \"30\")\n    val cartId: Long,\n\n    @Schema(description = \"전체 아이템 수량을 의미합니다\", defaultValue = \"3\")\n    val totalQuantity: Long,\n\n    @Schema(\n        description = \"결제가 필요한지에 대한 여부를 결정합니다. 필요한 true면 결제창 띄우시면됩니다. 이단계에선 무시하셔도 됩니다.\",\n        defaultValue = \"true\",\n    )\n    val isNeedPayment: Boolean,\n\n    @Schema(description = \"티켓의 타입. 승인 , 선착순 두가지입니다.\")\n    val approveType: TicketType,\n\n    @Schema(description = \"티켓의 지불 타입. 두둥티켓, 무료 , 유료 세가지입니다.\")\n    val ticketPayType: TicketPayType,\n\n    @Schema(description = \"계좌정보\", nullable = true)\n    val accountInfo: AccountInfoVo?,\n\n    @Schema(description = \"이벤트 정보\")\n    val eventProfile: EventProfileVo,\n) {\n    companion object {\n        @JvmStatic\n        fun of(cartItemResponses: List<CartItemResponse>, cart: Cart, item: TicketItem, event: Event): CartResponse {\n            return CartResponse(\n                items = cartItemResponses,\n                totalPrice = cart.getTotalPrice(),\n                cartId = cart.id!!,\n                title = cart.cartName!!,\n                isNeedPayment = cart.isNeedPaid(),\n                totalQuantity = cart.getTotalQuantity(),\n                approveType = item.type!!,\n                ticketPayType = item.payType!!,\n                accountInfo = item.accountInfo,\n                eventProfile = event.toEventProfileVo(),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/cart/model/mapper/CartMapper.kt",
    "content": "package band.gosrock.api.cart.model.mapper\n\nimport band.gosrock.api.cart.model.dto.request.AddCartLineDto\nimport band.gosrock.api.cart.model.dto.request.AddCartOptionAnswerDto\nimport band.gosrock.api.cart.model.dto.request.AddCartRequest\nimport band.gosrock.api.cart.model.dto.response.CartItemResponse\nimport band.gosrock.api.cart.model.dto.response.CartResponse\nimport band.gosrock.common.annotation.Mapper\nimport band.gosrock.domain.common.vo.OptionAnswerVo\nimport band.gosrock.domain.domains.cart.adaptor.CartAdaptor\nimport band.gosrock.domain.domains.cart.domain.Cart\nimport band.gosrock.domain.domains.cart.domain.CartLineItem\nimport band.gosrock.domain.domains.cart.domain.CartOptionAnswer\nimport band.gosrock.domain.domains.cart.domain.CartValidator\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.ticket_item.adaptor.OptionAdaptor\nimport band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor\nimport band.gosrock.domain.domains.ticket_item.domain.Option\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport org.springframework.transaction.annotation.Transactional\n\n@Mapper\nclass CartMapper(\n    private val ticketItemAdaptor: TicketItemAdaptor,\n    private val optionAdaptor: OptionAdaptor,\n    private val cartValidator: CartValidator,\n    private val eventAdaptor: EventAdaptor,\n    private val cartAdaptor: CartAdaptor,\n) {\n    @Transactional(readOnly = true)\n    fun toCartResponse(cartId: Long): CartResponse {\n        val cart = cartAdaptor.queryCart(cartId)\n        return getCartResponse(cart)\n    }\n\n    @Transactional(readOnly = true)\n    fun toCartResponse(cart: Cart): CartResponse {\n        return getCartResponse(cart)\n    }\n\n    private fun getCartResponse(cart: Cart): CartResponse {\n        val cartLineItems = cart.cartLineItems\n        val ticketItem = ticketItemAdaptor.queryTicketItem(cart.getItemId())\n        val event = eventAdaptor.findById(ticketItem.eventId!!)\n        val cartItemResponses = getCartItemResponses(cartLineItems, ticketItem.name!!)\n        return CartResponse.of(cartItemResponses, cart, ticketItem, event)\n    }\n\n    private fun getCartItemResponses(cartLineItems: List<CartLineItem>, name: String): List<CartItemResponse> {\n        return cartLineItems.map { getCartItemResponse(it, name) }\n    }\n\n    private fun getCartItemResponse(cartLineItem: CartLineItem, itemName: String): CartItemResponse {\n        return CartItemResponse.of(\n            cartLineItem,\n            generateCartLineName(itemName, cartLineItem.quantity!!),\n            getOptionAnswerVos(cartLineItem),\n        )\n    }\n\n    private fun getOptionAnswerVos(cartLineItem: CartLineItem): List<OptionAnswerVo> {\n        val cartOptionAnswers = cartLineItem.cartOptionAnswers\n        val options = optionAdaptor.findAllByIds(getOptionIds(cartOptionAnswers))\n        return cartOptionAnswers.map { cartOptionAnswer ->\n            cartOptionAnswer.getOptionAnswerVo(findOption(options, cartOptionAnswer))\n        }\n    }\n\n    private fun getOptionIds(cartOptionAnswers: List<CartOptionAnswer>): List<Long> {\n        return cartOptionAnswers.map { it.optionId!! }\n    }\n\n    private fun findOption(options: List<Option>, cartOptionAnswer: CartOptionAnswer): Option {\n        return options.first { it.id == cartOptionAnswer.optionId!! }\n    }\n\n    fun toEntity(addCartRequest: AddCartRequest, currentUserId: Long): Cart {\n        val addCartLineDtos = addCartRequest.items\n        val itemName = getItemName(addCartLineDtos)\n        val cartLineItems = addCartLineDtos.map { addCartLineDto ->\n            CartLineItem.of(\n                item = getTicketItem(addCartLineDto),\n                quantity = addCartLineDto.quantity,\n                cartOptionAnswers = getCartOptionAnswers(addCartLineDto),\n            )\n        }\n        return Cart.of(cartLineItems, itemName, currentUserId, cartValidator)\n    }\n\n    private fun getItemName(addCartLineDtos: List<AddCartLineDto>): String {\n        val itemId = addCartLineDtos.first().itemId\n        return ticketItemAdaptor.queryTicketItem(itemId).name!!\n    }\n\n    private fun getTicketItem(addCartLineDto: AddCartLineDto): TicketItem {\n        return ticketItemAdaptor.queryTicketItem(addCartLineDto.itemId)\n    }\n\n    private fun generateCartLineName(itemName: String, quantity: Long): String {\n        return \"$itemName ${quantity}매\"\n    }\n\n    private fun getCartOptionAnswers(addCartLineDto: AddCartLineDto): List<CartOptionAnswer> {\n        return addCartLineDto.options.map { getCartOptionAnswer(it) }\n    }\n\n    private fun getCartOptionAnswer(addCartOptionAnswerDto: AddCartOptionAnswerDto): CartOptionAnswer {\n        return CartOptionAnswer.of(\n            option = optionAdaptor.queryOption(addCartOptionAnswerDto.optionId),\n            answer = addCartOptionAnswerDto.answer ?: \"\",\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/cart/service/CheckOptionUseCase.kt",
    "content": "package band.gosrock.api.cart.service\n\nimport band.gosrock.common.annotation.UseCase\n\n@UseCase\nclass CheckOptionUseCase\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/cart/service/CreateCartUseCase.kt",
    "content": "package band.gosrock.api.cart.service\n\nimport band.gosrock.api.cart.model.dto.request.AddCartRequest\nimport band.gosrock.api.cart.model.dto.response.CartResponse\nimport band.gosrock.api.cart.model.mapper.CartMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.cart.service.CartDomainService\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass CreateCartUseCase(\n    private val cartDomainService: CartDomainService,\n    private val cartMapper: CartMapper,\n) {\n    @Transactional\n    fun execute(userId: Long, addCartRequest: AddCartRequest): CartResponse {\n        val cart = cartMapper.toEntity(addCartRequest, userId)\n        val cartId = cartDomainService.createCart(cart, userId)\n        return cartMapper.toCartResponse(cartId)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/cart/service/ReadCartUseCase.kt",
    "content": "package band.gosrock.api.cart.service\n\nimport band.gosrock.api.cart.model.dto.response.CartResponse\nimport band.gosrock.api.cart.model.mapper.CartMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.cart.adaptor.CartAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass ReadCartUseCase(\n    private val cartMapper: CartMapper,\n    private val cartAdaptor: CartAdaptor,\n) {\n    /** 내가 지금 가지고 있는 장바구니를 리턴합니다. 에러 상황은 아니기때문에 없으면 null 리턴합니다. */\n    @Transactional(readOnly = true)\n    fun execute(userId: Long): CartResponse? {\n        return cartAdaptor.findCartByUserId(userId)\n            .map { cartMapper.toCartResponse(it) }\n            .orElse(null)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/comment/controller/CommentController.kt",
    "content": "package band.gosrock.api.comment.controller\n\nimport band.gosrock.api.comment.model.request.CreateCommentRequest\nimport band.gosrock.api.comment.model.response.CreateCommentResponse\nimport band.gosrock.api.comment.model.response.RetrieveCommentCountResponse\nimport band.gosrock.api.comment.model.response.RetrieveCommentDTO\nimport band.gosrock.api.comment.model.response.RetrieveRandomCommentResponse\nimport band.gosrock.api.comment.service.CreateCommentUseCase\nimport band.gosrock.api.comment.service.DeleteCommentUseCase\nimport band.gosrock.api.comment.service.RetrieveCommentCountUseCase\nimport band.gosrock.api.comment.service.RetrieveCommentUseCase\nimport band.gosrock.api.comment.service.RetrieveRandomCommentUseCase\nimport band.gosrock.api.common.slice.SliceResponse\nimport band.gosrock.common.annotation.CurrentUserId\nimport band.gosrock.common.annotation.DisableSwaggerSecurity\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport jakarta.validation.Valid\nimport jakarta.validation.constraints.Min\nimport org.springdoc.core.annotations.ParameterObject\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.web.PageableDefault\nimport org.springframework.validation.annotation.Validated\nimport org.springframework.web.bind.annotation.DeleteMapping\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestBody\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RequestParam\nimport org.springframework.web.bind.annotation.RestController\n\n@SecurityRequirement(name = \"access-token\")\n@Tag(name = \"9. [응원톡]\")\n@RestController\n@RequestMapping(\"/api/v1/events/{eventId}/comments\")\n@Validated\nclass CommentController(\n    private val createCommentUseCase: CreateCommentUseCase,\n    private val retrieveCommentUseCase: RetrieveCommentUseCase,\n    private val deleteCommentUseCase: DeleteCommentUseCase,\n    private val retrieveCommentCountUseCase: RetrieveCommentCountUseCase,\n    private val retrieveRandomCommentUseCase: RetrieveRandomCommentUseCase,\n) {\n    @Operation(summary = \"응원글을 생성합니다.\")\n    @PostMapping\n    fun postComment(\n        @CurrentUserId userId: Long,\n        @RequestBody @Valid createCommentRequest: CreateCommentRequest,\n        @PathVariable eventId: Long,\n    ): CreateCommentResponse = createCommentUseCase.execute(userId, eventId, createCommentRequest)\n\n    @DisableSwaggerSecurity\n    @Operation(summary = \"응원글을 조회합니다.\")\n    @GetMapping\n    fun getComments(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @ParameterObject @PageableDefault(size = 10) pageable: Pageable,\n    ): SliceResponse<RetrieveCommentDTO> = retrieveCommentUseCase.execute(userId, eventId, pageable)\n\n    @Operation(summary = \"[어드민 기능] 응원글을 삭제합니다.\")\n    @DeleteMapping(\"/{commentId}\")\n    fun deleteComment(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @PathVariable commentId: Long,\n    ): Unit = deleteCommentUseCase.execute(userId, eventId, commentId)\n\n    @DisableSwaggerSecurity\n    @Operation(summary = \"응원글 개수를 카운팅합니다.\")\n    @GetMapping(\"/counts\")\n    fun getCommentCounts(\n        @PathVariable eventId: Long,\n    ): RetrieveCommentCountResponse = retrieveCommentCountUseCase.execute(eventId)\n\n    @DisableSwaggerSecurity\n    @Operation(summary = \"응원글을 랜덤으로 뽑아옵니다.\")\n    @GetMapping(\"/random\")\n    fun getRandomComment(\n        @PathVariable eventId: Long,\n        @RequestParam @Min(value = 1L, message = \"limit 값은 0보다 커야 합니다.\") limit: Long,\n    ): RetrieveRandomCommentResponse = retrieveRandomCommentUseCase.execute(eventId, limit)\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/comment/mapper/CommentMapper.kt",
    "content": "package band.gosrock.api.comment.mapper\n\nimport band.gosrock.api.comment.model.request.CreateCommentRequest\nimport band.gosrock.api.comment.model.response.CreateCommentResponse\nimport band.gosrock.api.comment.model.response.RetrieveCommentCountResponse\nimport band.gosrock.api.comment.model.response.RetrieveCommentDTO\nimport band.gosrock.api.comment.model.response.RetrieveRandomCommentResponse\nimport band.gosrock.api.common.slice.SliceResponse\nimport band.gosrock.common.annotation.Mapper\nimport band.gosrock.domain.domains.comment.adaptor.CommentAdaptor\nimport band.gosrock.domain.domains.comment.domain.Comment\nimport band.gosrock.domain.domains.comment.dto.condition.CommentCondition\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.user.domain.User\nimport org.springframework.transaction.annotation.Transactional\n\n@Mapper\nclass CommentMapper(\n    private val commentAdaptor: CommentAdaptor,\n) {\n    fun toEntity(user: User, event: Event, createDTO: CreateCommentRequest): Comment =\n        Comment.create(createDTO.content, createDTO.nickName, user, event.id!!)\n\n    @Transactional(readOnly = true)\n    fun toCreateCommentResponse(comment: Comment, user: User): CreateCommentResponse =\n        CreateCommentResponse.of(comment, user)\n\n    @Transactional(readOnly = true)\n    fun toRetrieveCommentListResponse(\n        commentCondition: CommentCondition,\n        currentUserId: Long?,\n    ): SliceResponse<RetrieveCommentDTO> {\n        val comments = commentAdaptor.searchComment(commentCondition)\n        return SliceResponse.of(comments.map { toRetrieveCommentDTO(it, currentUserId) })\n    }\n\n    @Transactional(readOnly = true)\n    fun retrieveComment(commentId: Long): Comment =\n        commentAdaptor.queryComment(commentId)\n\n    fun toRetrieveCommentCountResponse(commentCount: Long?): RetrieveCommentCountResponse =\n        RetrieveCommentCountResponse.of(commentCount)\n\n    fun toRetrieveRandomCommentResponse(comments: List<Comment>): RetrieveRandomCommentResponse =\n        RetrieveRandomCommentResponse.of(comments)\n\n    private fun toRetrieveCommentDTO(comment: Comment, currentUserId: Long?): RetrieveCommentDTO =\n        RetrieveCommentDTO.of(comment, currentUserId)\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/comment/model/request/CreateCommentRequest.kt",
    "content": "package band.gosrock.api.comment.model.request\n\nimport jakarta.validation.constraints.NotBlank\nimport jakarta.validation.constraints.Size\n\ndata class CreateCommentRequest(\n    @field:NotBlank(message = \"작성자 닉네임을 입력해주세요.\")\n    @field:Size(min = 1, max = 10)\n    val nickName: String,\n\n    @field:NotBlank(message = \"댓글 내용을 입력해주세요.\")\n    @field:Size(min = 1, max = 150)\n    val content: String,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/comment/model/response/CreateCommentResponse.kt",
    "content": "package band.gosrock.api.comment.model.response\n\nimport band.gosrock.common.annotation.DateFormat\nimport band.gosrock.domain.common.vo.UserInfoVo\nimport band.gosrock.domain.domains.comment.domain.Comment\nimport band.gosrock.domain.domains.user.domain.User\nimport java.time.LocalDateTime\n\ndata class CreateCommentResponse(\n    val id: Long?,\n    val nickName: String?,\n    val content: String?,\n    @DateFormat val createdAt: LocalDateTime?,\n    val userInfoVo: UserInfoVo?,\n) {\n    companion object {\n        @JvmStatic\n        fun of(comment: Comment, user: User): CreateCommentResponse =\n            CreateCommentResponse(\n                id = comment.id,\n                nickName = comment.nickName,\n                content = comment.content,\n                createdAt = comment.createdAt,\n                userInfoVo = user.toUserInfoVo(),\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/comment/model/response/RetrieveCommentCountResponse.kt",
    "content": "package band.gosrock.api.comment.model.response\n\ndata class RetrieveCommentCountResponse(\n    val commentCounts: Long?,\n) {\n    companion object {\n        @JvmStatic\n        fun of(commentCounts: Long?): RetrieveCommentCountResponse =\n            RetrieveCommentCountResponse(commentCounts = commentCounts)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/comment/model/response/RetrieveCommentDTO.kt",
    "content": "package band.gosrock.api.comment.model.response\n\nimport band.gosrock.domain.common.vo.CommentInfoVo\nimport band.gosrock.domain.domains.comment.domain.Comment\n\ndata class RetrieveCommentDTO(\n    val commentInfo: CommentInfoVo?,\n    val isMine: Boolean?,\n) {\n    companion object {\n        @JvmStatic\n        fun of(comment: Comment, currentUserId: Long?): RetrieveCommentDTO =\n            RetrieveCommentDTO(\n                commentInfo = comment.toCommentInfoVo(),\n                isMine = comment.user?.id == currentUserId,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/comment/model/response/RetrieveCommentListResponse.kt",
    "content": "package band.gosrock.api.comment.model.response\n\nimport band.gosrock.domain.domains.comment.domain.Comment\nimport org.springframework.data.domain.Slice\n\ndata class RetrieveCommentListResponse(\n    val hasNext: Boolean?,\n    val comments: List<RetrieveCommentDTO>,\n) {\n    companion object {\n        @JvmStatic\n        fun of(comments: Slice<Comment>, currentUserId: Long?): RetrieveCommentListResponse =\n            RetrieveCommentListResponse(\n                hasNext = comments.hasNext(),\n                comments = comments.map { RetrieveCommentDTO.of(it, currentUserId) }.toList(),\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/comment/model/response/RetrieveRandomCommentResponse.kt",
    "content": "package band.gosrock.api.comment.model.response\n\nimport band.gosrock.domain.common.vo.CommentInfoVo\nimport band.gosrock.domain.domains.comment.domain.Comment\n\ndata class RetrieveRandomCommentResponse(\n    val commentInfos: List<CommentInfoVo?>,\n) {\n    companion object {\n        @JvmStatic\n        fun of(comments: List<Comment>): RetrieveRandomCommentResponse =\n            RetrieveRandomCommentResponse(\n                commentInfos = comments.map { it.toCommentInfoVo() },\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/comment/service/CreateCommentUseCase.kt",
    "content": "package band.gosrock.api.comment.service\n\nimport band.gosrock.api.comment.mapper.CommentMapper\nimport band.gosrock.api.comment.model.request.CreateCommentRequest\nimport band.gosrock.api.comment.model.response.CreateCommentResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.comment.adaptor.CommentAdaptor\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass CreateCommentUseCase(\n    private val userAdaptor: UserAdaptor,\n    private val commentMapper: CommentMapper,\n    private val commentAdaptor: CommentAdaptor,\n    private val eventAdaptor: EventAdaptor,\n) {\n    @Transactional\n    fun execute(userId: Long, eventId: Long, createDTO: CreateCommentRequest): CreateCommentResponse {\n        val currentUser = userAdaptor.queryUser(userId)\n        val event = eventAdaptor.findById(eventId)\n        val comment = commentAdaptor.save(commentMapper.toEntity(currentUser, event, createDTO))\n        return commentMapper.toCreateCommentResponse(comment, currentUser)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/comment/service/DeleteCommentUseCase.kt",
    "content": "package band.gosrock.api.comment.service\n\nimport band.gosrock.api.comment.mapper.CommentMapper\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.MANAGER\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.comment.service.CommentDomainService\nimport band.gosrock.domain.domains.event.service.EventService\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass DeleteCommentUseCase(\n    private val commentMapper: CommentMapper,\n    private val eventService: EventService,\n    private val commentDomainService: CommentDomainService,\n) {\n    @Transactional\n    @HostRolesAllowed(role = MANAGER, findHostFrom = EVENT_ID)\n    fun execute(userId: Long, eventId: Long, commentId: Long) {\n        val comment = commentMapper.retrieveComment(commentId)\n        commentDomainService.deleteComment(comment, eventId)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/comment/service/RetrieveCommentCountUseCase.kt",
    "content": "package band.gosrock.api.comment.service\n\nimport band.gosrock.api.comment.mapper.CommentMapper\nimport band.gosrock.api.comment.model.response.RetrieveCommentCountResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.comment.adaptor.CommentAdaptor\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass RetrieveCommentCountUseCase(\n    private val commentAdaptor: CommentAdaptor,\n    private val commentMapper: CommentMapper,\n    private val eventAdaptor: EventAdaptor,\n) {\n    @Transactional(readOnly = true)\n    fun execute(eventId: Long): RetrieveCommentCountResponse {\n        val event = eventAdaptor.findById(eventId)\n        val commentCount = commentAdaptor.queryCommentCount(event.id!!)\n        return commentMapper.toRetrieveCommentCountResponse(commentCount)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/comment/service/RetrieveCommentUseCase.kt",
    "content": "package band.gosrock.api.comment.service\n\nimport band.gosrock.api.comment.mapper.CommentMapper\nimport band.gosrock.api.comment.model.response.RetrieveCommentDTO\nimport band.gosrock.api.common.slice.SliceResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.comment.dto.condition.CommentCondition\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport org.springframework.data.domain.Pageable\n\n@UseCase\nclass RetrieveCommentUseCase(\n    private val commentMapper: CommentMapper,\n    private val eventAdaptor: EventAdaptor,\n) {\n    fun execute(userId: Long, eventId: Long, pageable: Pageable): SliceResponse<RetrieveCommentDTO> {\n        val event = eventAdaptor.findById(eventId)\n        val commentCondition = CommentCondition(event.id!!, pageable)\n        return commentMapper.toRetrieveCommentListResponse(commentCondition, userId)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/comment/service/RetrieveRandomCommentUseCase.kt",
    "content": "package band.gosrock.api.comment.service\n\nimport band.gosrock.api.comment.mapper.CommentMapper\nimport band.gosrock.api.comment.model.response.RetrieveRandomCommentResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.comment.adaptor.CommentAdaptor\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass RetrieveRandomCommentUseCase(\n    private val commentAdaptor: CommentAdaptor,\n    private val commentMapper: CommentMapper,\n    private val eventAdaptor: EventAdaptor,\n) {\n    @Transactional(readOnly = true)\n    fun execute(eventId: Long, limit: Long): RetrieveRandomCommentResponse {\n        val event = eventAdaptor.findById(eventId)\n        val comments = commentAdaptor.queryRandomComment(event.id!!, limit)\n        return commentMapper.toRetrieveRandomCommentResponse(comments)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/UserUtils.kt",
    "content": "package band.gosrock.api.common\n\nimport band.gosrock.api.config.security.SecurityUtils\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.domain.domains.user.domain.User\nimport org.springframework.stereotype.Component\n\n@Component\nclass UserUtils(\n    private val userAdaptor: UserAdaptor,\n) {\n    fun getCurrentUserId(): Long = SecurityUtils.getCurrentUserId()\n\n    fun getCurrentUser(): User = userAdaptor.queryUser(getCurrentUserId())\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/aop/hostPartner/HostPartnerAllowed.kt",
    "content": "package band.gosrock.api.common.aop.hostPartner\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom\nimport java.lang.annotation.Retention\nimport java.lang.annotation.RetentionPolicy\n\n/** HostPartner aop 를 적용하기 위해 다는 어노테이션 - 이찬진 */\n@Target(AnnotationTarget.FUNCTION)\n@Retention(RetentionPolicy.RUNTIME)\nannotation class HostPartnerAllowed(\n    val findHostFrom: FindHostFrom\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/aop/hostPartner/HostPartnerAop.kt",
    "content": "package band.gosrock.api.common.aop.hostPartner\n\nimport org.aspectj.lang.ProceedingJoinPoint\nimport org.aspectj.lang.annotation.Around\nimport org.aspectj.lang.annotation.Aspect\nimport org.aspectj.lang.reflect.MethodSignature\nimport org.slf4j.LoggerFactory\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnExpression\nimport org.springframework.stereotype.Component\n\n/**\n * 호스트 관리자 인가를 위한 aop 입니다 메소드 레벨에서 작동하며 권한 정보를 어노테이션으로 받고 eventId를 인자에서 찾아와 호스트 정보를 불러온뒤 권한 검증을 합니다.\n */\n@Aspect\n@Component\n@ConditionalOnExpression(\"\\${ableHostPartnerAop:true}\")\ninternal class HostPartnerAop(\n    private val hostPartnerCallTransactionFactory: HostPartnerCallTransactionFactory\n) {\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    /**\n     * master 호스트의 마스터 manager 호스트의 수정,조회 ( 호스트유저도메인의 슈퍼 호스트 ) guest 호스트의 조회권한 (호스트유저도메인의 호스트 )\n     *\n     * @see band.gosrock.domain.domains.host.domain.HostRole\n     */\n    @Around(\"@annotation(band.gosrock.api.common.aop.hostPartner.HostPartnerAllowed)\")\n    @Throws(Throwable::class)\n    fun aop(joinPoint: ProceedingJoinPoint): Any? {\n        val signature = joinPoint.signature as MethodSignature\n        val method = signature.method\n        val annotation = method.getAnnotation(HostPartnerAllowed::class.java)\n        val findHostFrom = annotation.findHostFrom\n        val identifier = findHostFrom.identifier\n\n        val parameterNames = signature.parameterNames\n        val args = joinPoint.args\n\n        val id = getId(parameterNames, args, identifier)\n\n        return hostPartnerCallTransactionFactory\n            .getCallTransaction(findHostFrom)\n            .proceed(id, joinPoint)\n    }\n\n    fun getId(parameterNames: Array<String>, args: Array<Any?>, paramName: String): Long {\n        for (i in parameterNames.indices) {\n            if (parameterNames[i] == paramName) {\n                return args[i] as Long\n            }\n        }\n        throw IllegalArgumentException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/aop/hostPartner/HostPartnerCallTransaction.kt",
    "content": "package band.gosrock.api.common.aop.hostPartner\n\nimport org.aspectj.lang.ProceedingJoinPoint\n\ninternal interface HostPartnerCallTransaction {\n    @Throws(Throwable::class)\n    fun proceed(id: Long, joinPoint: ProceedingJoinPoint): Any?\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/aop/hostPartner/HostPartnerCallTransactionFactory.kt",
    "content": "package band.gosrock.api.common.aop.hostPartner\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom\nimport org.springframework.stereotype.Component\n\n@Component\ninternal class HostPartnerCallTransactionFactory(\n    private val hostRoleEventTransaction: HostPartnerEventTransaction,\n    private val hostRoleHostTransaction: HostPartnerHostTransaction\n) {\n    fun getCallTransaction(findHostFrom: FindHostFrom): HostPartnerCallTransaction {\n        return if (findHostFrom == FindHostFrom.HOST_ID) {\n            hostRoleHostTransaction\n        } else {\n            hostRoleEventTransaction\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/aop/hostPartner/HostPartnerEventTransaction.kt",
    "content": "package band.gosrock.api.common.aop.hostPartner\n\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport org.aspectj.lang.ProceedingJoinPoint\nimport org.slf4j.LoggerFactory\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.annotation.Transactional\n\n/** 호스트 정보를 트랜잭션 안에서 조회하기 위해서 만든 클래스입니다. 트랜잭션 내에서 캐시 할수 있으면 좋으니 이렇게 만들었습니다. - 이찬진 */\n@Component\ninternal class HostPartnerEventTransaction(\n    private val eventAdaptor: EventAdaptor,\n    private val hostAdaptor: HostAdaptor\n) : HostPartnerCallTransaction {\n\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    @Transactional(readOnly = true)\n    override fun proceed(eventId: Long, joinPoint: ProceedingJoinPoint): Any? {\n        val event = eventAdaptor.findById(eventId)\n        val host = hostAdaptor.findById(event.hostId!!)\n        host.validatePartnerHost()\n        return joinPoint.proceed()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/aop/hostPartner/HostPartnerHostTransaction.kt",
    "content": "package band.gosrock.api.common.aop.hostPartner\n\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport org.aspectj.lang.ProceedingJoinPoint\nimport org.slf4j.LoggerFactory\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.annotation.Transactional\n\n/** 호스트 정보를 트랜잭션 안에서 조회하기 위해서 만든 클래스입니다. 트랜잭션 내에서 캐시 할수 있으면 좋으니 이렇게 만들었습니다. - 이찬진 */\n@Component\ninternal class HostPartnerHostTransaction(\n    private val hostAdaptor: HostAdaptor\n) : HostPartnerCallTransaction {\n\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    @Transactional(readOnly = true)\n    override fun proceed(hostId: Long, joinPoint: ProceedingJoinPoint): Any? {\n        val host = hostAdaptor.findById(hostId)\n        host.validatePartnerHost()\n        return joinPoint.proceed()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/aop/hostRole/FindHostFrom.kt",
    "content": "package band.gosrock.api.common.aop.hostRole\n\nenum class FindHostFrom(val identifier: String) {\n    HOST_ID(\"hostId\"),\n    EVENT_ID(\"eventId\")\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/aop/hostRole/HostCallTransactionFactory.kt",
    "content": "package band.gosrock.api.common.aop.hostRole\n\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.stereotype.Component\n\n@Component\ninternal class HostCallTransactionFactory(\n    userAdaptor: UserAdaptor,\n    hostAdaptor: HostAdaptor,\n    eventAdaptor: EventAdaptor,\n    hostRoleEventTransaction: HostRoleEventTransaction,\n    hostRoleHostTransaction: HostRoleHostTransaction\n) {\n    private val hostRoleEventTransaction: HostRoleEventTransaction = hostRoleEventTransaction\n    private val hostRoleEventWithoutTransaction: HostRoleEventTransaction =\n        HostRoleEventTransaction(userAdaptor, eventAdaptor, hostAdaptor)\n    private val hostRoleHostTransaction: HostRoleHostTransaction = hostRoleHostTransaction\n    private val hostRoleHostWithoutTransaction: HostRoleHostTransaction =\n        HostRoleHostTransaction(userAdaptor, hostAdaptor)\n\n    fun getCallTransaction(findHostFrom: FindHostFrom, applyTransaction: Boolean): HostRoleCallTransaction {\n        return if (findHostFrom == FindHostFrom.HOST_ID) {\n            if (applyTransaction) hostRoleHostTransaction else hostRoleHostWithoutTransaction\n        } else {\n            if (applyTransaction) hostRoleEventTransaction else hostRoleEventWithoutTransaction\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/aop/hostRole/HostQualification.kt",
    "content": "package band.gosrock.api.common.aop.hostRole\n\nimport band.gosrock.domain.domains.host.domain.Host\n\n/** 각 권한에 맞춰서 host 도메인의 검증메소드를 실행시킵니다. 검증 메소드이므로 biConsumer 를 통해 실행 시킬 함수를 미리 생성해 둡니다. -이찬진 */\nenum class HostQualification(private val consumer: (Long, Host) -> Unit) {\n    MASTER({ userId, host -> host.validateMasterHostUser(userId) }),\n    MANAGER({ userId, host -> host.validateManagerHostUser(userId) }),\n    GUEST({ userId, host -> host.validateActiveHostUser(userId) });\n\n    /**\n     * 호스트의 검증을 수행하는 메서드\n     */\n    fun validQualification(userId: Long, host: Host) {\n        consumer(userId, host)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/aop/hostRole/HostRoleAop.kt",
    "content": "package band.gosrock.api.common.aop.hostRole\n\nimport org.aspectj.lang.ProceedingJoinPoint\nimport org.aspectj.lang.annotation.Around\nimport org.aspectj.lang.annotation.Aspect\nimport org.aspectj.lang.reflect.MethodSignature\nimport org.slf4j.LoggerFactory\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnExpression\nimport org.springframework.stereotype.Component\n\n/**\n * 호스트 관리자 인가를 위한 aop 입니다 메소드 레벨에서 작동하며 권한 정보를 어노테이션으로 받고 eventId를 인자에서 찾아와 호스트 정보를 불러온뒤 권한 검증을 합니다.\n */\n@Aspect\n@Component\n@ConditionalOnExpression(\"\\${ableHostRoleAop:true}\")\ninternal class HostRoleAop(\n    private val hostCallTransactionFactory: HostCallTransactionFactory\n) {\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    /**\n     * master 호스트의 마스터 manager 호스트의 수정,조회 ( 호스트유저도메인의 슈퍼 호스트 ) guest 호스트의 조회권한 (호스트유저도메인의 호스트 )\n     *\n     * @see band.gosrock.domain.domains.host.domain.HostRole\n     */\n    @Around(\"@annotation(band.gosrock.api.common.aop.hostRole.HostRolesAllowed)\")\n    @Throws(Throwable::class)\n    fun aop(joinPoint: ProceedingJoinPoint): Any? {\n        val signature = joinPoint.signature as MethodSignature\n        val method = signature.method\n        val annotation = method.getAnnotation(HostRolesAllowed::class.java)\n        val hostQualification = annotation.role\n        val findHostFrom = annotation.findHostFrom\n        val identifier = findHostFrom.identifier\n\n        val parameterNames = signature.parameterNames\n        val args = joinPoint.args\n\n        val userId = getId(parameterNames, args, \"userId\")\n        val id = getId(parameterNames, args, identifier)\n\n        return hostCallTransactionFactory\n            .getCallTransaction(findHostFrom, annotation.applyTransaction)\n            .proceed(userId, id, hostQualification, joinPoint)\n    }\n\n    fun getId(parameterNames: Array<String>, args: Array<Any?>, paramName: String): Long {\n        for (i in parameterNames.indices) {\n            if (parameterNames[i] == paramName) {\n                return args[i] as Long\n            }\n        }\n        throw IllegalArgumentException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/aop/hostRole/HostRoleCallTransaction.kt",
    "content": "package band.gosrock.api.common.aop.hostRole\n\nimport org.aspectj.lang.ProceedingJoinPoint\n\ninternal interface HostRoleCallTransaction {\n    @Throws(Throwable::class)\n    fun proceed(userId: Long, id: Long, role: HostQualification, joinPoint: ProceedingJoinPoint): Any?\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/aop/hostRole/HostRoleEventTransaction.kt",
    "content": "package band.gosrock.api.common.aop.hostRole\n\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.domain.domains.user.domain.AccountRole\nimport org.aspectj.lang.ProceedingJoinPoint\nimport org.slf4j.LoggerFactory\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.annotation.Transactional\n\n/** 호스트 정보를 트랜잭션 안에서 조회하기 위해서 만든 클래스입니다. 트랜잭션 내에서 캐시 할수 있으면 좋으니 이렇게 만들었습니다. - 이찬진 */\n@Component\ninternal class HostRoleEventTransaction(\n    private val userAdaptor: UserAdaptor,\n    private val eventAdaptor: EventAdaptor,\n    private val hostAdaptor: HostAdaptor\n) : HostRoleCallTransaction {\n\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    @Transactional(readOnly = true)\n    override fun proceed(userId: Long, eventId: Long, role: HostQualification, joinPoint: ProceedingJoinPoint): Any? {\n        validRole(userId, eventId, role)\n        return joinPoint.proceed()\n    }\n\n    private fun validRole(userId: Long, eventId: Long, role: HostQualification) {\n        val user = userAdaptor.queryUser(userId)\n        if (user.accountRole == AccountRole.SUPER_ADMIN) {\n            log.info(\"[AUTH] SUPER_ADMIN bypass - userId={}, eventId={}\", userId, eventId)\n            return\n        }\n        val event = eventAdaptor.findById(eventId)\n        val host = hostAdaptor.findById(event.hostId!!)\n        role.validQualification(userId, host)\n        log.info(\"[AUTH] Host role verified - userId={}, eventId={}, required={}\", userId, eventId, role)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/aop/hostRole/HostRoleHostTransaction.kt",
    "content": "package band.gosrock.api.common.aop.hostRole\n\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.domain.domains.user.domain.AccountRole\nimport org.aspectj.lang.ProceedingJoinPoint\nimport org.slf4j.LoggerFactory\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.annotation.Transactional\n\n/** 호스트 정보를 트랜잭션 안에서 조회하기 위해서 만든 클래스입니다. 트랜잭션 내에서 캐시 할수 있으면 좋으니 이렇게 만들었습니다. - 이찬진 */\n@Component\ninternal class HostRoleHostTransaction(\n    private val userAdaptor: UserAdaptor,\n    private val hostAdaptor: HostAdaptor\n) : HostRoleCallTransaction {\n\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    @Transactional(readOnly = true)\n    override fun proceed(userId: Long, hostId: Long, role: HostQualification, joinPoint: ProceedingJoinPoint): Any? {\n        validRole(userId, hostId, role)\n        return joinPoint.proceed()\n    }\n\n    private fun validRole(userId: Long, hostId: Long, role: HostQualification) {\n        val user = userAdaptor.queryUser(userId)\n        if (user.accountRole == AccountRole.SUPER_ADMIN) {\n            log.info(\"[AUTH] SUPER_ADMIN bypass - userId={}, hostId={}\", userId, hostId)\n            return\n        }\n        val host = hostAdaptor.findById(hostId)\n        role.validQualification(userId, host)\n        log.info(\"[AUTH] Host role verified - userId={}, hostId={}, required={}\", userId, hostId, role)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/aop/hostRole/HostRolesAllowed.kt",
    "content": "package band.gosrock.api.common.aop.hostRole\n\nimport java.lang.annotation.Retention\nimport java.lang.annotation.RetentionPolicy\n\n/** HostRoles aop 를 적용하기 위해 다는 어노테이션 - 이찬진 */\n@Target(AnnotationTarget.FUNCTION)\n@Retention(RetentionPolicy.RUNTIME)\nannotation class HostRolesAllowed(\n    /**\n     * 세가지 값을 가짐 \"MASTER\",\"MANAGER\",\"GUEST\" 권한 정보는\n     *\n     * @see HostRoleAop\n     */\n    val role: HostQualification,\n    val findHostFrom: FindHostFrom,\n    val applyTransaction: Boolean = true\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/customizer/EnumValuePropertyCustomizer.kt",
    "content": "package band.gosrock.api.common.customizer\n\nimport com.fasterxml.jackson.databind.JavaType\nimport io.swagger.v3.core.converter.AnnotatedType\nimport io.swagger.v3.core.util.Json\nimport io.swagger.v3.oas.models.media.Schema\nimport io.swagger.v3.oas.models.media.StringSchema\nimport org.springdoc.core.customizers.PropertyCustomizer\nimport org.springframework.stereotype.Component\n\n// https://stackoverflow.com/questions/68747036/how-can-have-springdoc-openapi-use-the-jsonvalue-enum-format-without-changing-t\n\n/** enum jsonvalue 어노테이션 사용할때 예시값을 보여주기 위함. */\n@Component\nclass EnumValuePropertyCustomizer : PropertyCustomizer {\n    override fun customize(property: Schema<*>, type: AnnotatedType): Schema<*> {\n        if (property is StringSchema && isEnumType(type)) {\n            val objectMapper = Json.mapper()\n            property.setEnum(\n                (type.type as JavaType).rawClass.enumConstants\n                    ?.map { e -> objectMapper.convertValue(e, String::class.java) }\n                    ?: emptyList()\n            )\n        }\n        return property\n    }\n\n    private fun isEnumType(type: AnnotatedType): Boolean {\n        return type.type is JavaType && (type.type as JavaType).isEnumType\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/page/PageResponse.kt",
    "content": "package band.gosrock.api.common.page\n\nimport org.springframework.data.domain.Page\n\ndata class PageResponse<T>(\n    val content: List<T>,\n    val page: Int,\n    val size: Int,\n    val totalElements: Long,\n    val totalPages: Int,\n    val hasNextPage: Boolean,\n) {\n    companion object {\n        @JvmStatic\n        fun <T> of(page: Page<T>): PageResponse<T> =\n            PageResponse(\n                content = page.content,\n                page = page.number,\n                size = page.numberOfElements,\n                totalElements = page.totalElements,\n                totalPages = page.totalPages,\n                hasNextPage = page.hasNext(),\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/slice/SliceParam.kt",
    "content": "package band.gosrock.api.common.slice\n\nimport io.swagger.v3.oas.annotations.media.Schema\nimport org.springframework.data.domain.PageRequest\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.domain.Sort\nimport jakarta.validation.constraints.Positive\n\n@Deprecated(\"Use cursor-based pagination instead\")\nclass SliceParam(\n    @field:Schema(description = \"현재 조회하려는 페이지\")\n    @field:Positive\n    var page: Int? = null,\n\n    @field:Schema(description = \"한 번 조회당 레코드 개수 (default: 10)\")\n    @field:Positive\n    var size: Int? = null,\n\n    @field:Schema(description = \"정렬 조건 프로퍼티 (default: \\\"id\\\")\")\n    var sort: String? = null,\n\n    @field:Schema(description = \"정렬 순서 (ASC | DESC) (default: DESC)\")\n    var direction: Sort.Direction? = null,\n) {\n    fun toPageable(): Pageable {\n        // RequestParam 에 빈 값은 null 로 할당되어 검증 필요\n        if (page == null) page = 0\n        if (size == null) size = 10\n        if (sort == null) sort = \"id\"\n        if (direction == null) direction = Sort.Direction.DESC\n        return PageRequest.of(page!!, size!!, Sort.by(direction!!, sort!!))\n    }\n\n    companion object {\n        @JvmStatic\n        fun pageableOf(sliceParam: SliceParam): Pageable = sliceParam.toPageable()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/common/slice/SliceResponse.kt",
    "content": "package band.gosrock.api.common.slice\n\nimport org.springframework.data.domain.Slice\n\ndata class SliceResponse<T>(\n    val content: List<T>,\n    val page: Long,\n    val size: Int,\n    val hasNext: Boolean,\n) {\n    companion object {\n        @JvmStatic\n        fun <T> of(slice: Slice<T>): SliceResponse<T> =\n            SliceResponse(\n                content = slice.content,\n                page = slice.number.toLong(),\n                size = slice.numberOfElements,\n                hasNext = slice.hasNext(),\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/ExampleHolder.kt",
    "content": "package band.gosrock.api.config\n\nimport io.swagger.v3.oas.models.examples.Example\n\ndata class ExampleHolder(\n    val holder: Example,\n    val name: String,\n    val code: Int,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/HttpContentCacheFilter.kt",
    "content": "package band.gosrock.api.config\n\nimport org.springframework.stereotype.Component\nimport org.springframework.web.filter.OncePerRequestFilter\nimport org.springframework.web.util.ContentCachingRequestWrapper\nimport org.springframework.web.util.ContentCachingResponseWrapper\nimport jakarta.servlet.FilterChain\nimport jakarta.servlet.http.HttpServletRequest\nimport jakarta.servlet.http.HttpServletResponse\n\n@Component\nclass HttpContentCacheFilter : OncePerRequestFilter() {\n    override fun doFilterInternal(\n        request: HttpServletRequest,\n        response: HttpServletResponse,\n        chain: FilterChain,\n    ) {\n        val wrappingRequest = ContentCachingRequestWrapper(request)\n        val wrappingResponse = ContentCachingResponseWrapper(response)\n\n        chain.doFilter(wrappingRequest, wrappingResponse)\n\n        wrappingResponse.copyBodyToResponse()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/MdcFilter.kt",
    "content": "package band.gosrock.api.config\n\nimport jakarta.servlet.FilterChain\nimport jakarta.servlet.http.HttpServletRequest\nimport jakarta.servlet.http.HttpServletResponse\nimport org.slf4j.LoggerFactory\nimport org.slf4j.MDC\nimport org.springframework.stereotype.Component\nimport org.springframework.web.filter.OncePerRequestFilter\nimport org.springframework.web.util.ContentCachingRequestWrapper\nimport java.util.UUID\n\n@Component\nclass MdcFilter : OncePerRequestFilter() {\n\n    private val log = LoggerFactory.getLogger(MdcFilter::class.java)\n\n    companion object {\n        private const val MAX_BODY_LOG_SIZE = 2048\n        private val EXCLUDED_PATHS = listOf(\"/api/v1/auth\", \"/api/v1/payment\")\n    }\n\n    override fun doFilterInternal(\n        request: HttpServletRequest,\n        response: HttpServletResponse,\n        filterChain: FilterChain,\n    ) {\n        val wrappedRequest = ContentCachingRequestWrapper(request, MAX_BODY_LOG_SIZE)\n        try {\n            val traceId = request.getHeader(\"X-Trace-Id\")\n                ?: UUID.randomUUID().toString().replace(\"-\", \"\").substring(0, 8)\n            MDC.put(\"traceId\", traceId)\n            response.setHeader(\"X-Trace-Id\", traceId)\n            filterChain.doFilter(wrappedRequest, response)\n        } finally {\n            logRequest(wrappedRequest, response)\n            MDC.clear()\n        }\n    }\n\n    private fun logRequest(request: ContentCachingRequestWrapper, response: HttpServletResponse) {\n        val uri = request.requestURI\n        val method = request.method\n        val status = response.status\n\n        val body = if (shouldLogBody(uri, method)) {\n            val content = request.contentAsByteArray\n            if (content.isNotEmpty()) {\n                String(content, Charsets.UTF_8).take(MAX_BODY_LOG_SIZE)\n            } else {\n                null\n            }\n        } else {\n            \"[FILTERED]\"\n        }\n\n        if (body != null) {\n            log.info(\"{} {} {} body={}\", method, uri, status, body)\n        } else {\n            log.info(\"{} {} {}\", method, uri, status)\n        }\n    }\n\n    private fun shouldLogBody(uri: String, method: String): Boolean {\n        if (method == \"GET\") return false\n        return EXCLUDED_PATHS.none { uri.startsWith(it) }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/ServletFilterConfig.kt",
    "content": "package band.gosrock.api.config\n\nimport org.springframework.boot.web.servlet.FilterRegistrationBean\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\nimport org.springframework.context.annotation.Profile\nimport org.springframework.web.filter.ForwardedHeaderFilter\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer\nimport org.springframework.web.servlet.resource.ResourceUrlEncodingFilter\n\n@Configuration\n@Profile(\"prod\", \"staging\", \"dev\")\nclass ServletFilterConfig(\n    private val httpContentCacheFilter: HttpContentCacheFilter,\n    private val forwardedHeaderFilter: ForwardedHeaderFilter,\n    private val mdcFilter: MdcFilter,\n) : WebMvcConfigurer {\n\n    @Bean\n    fun setMdcFilterOrder(): FilterRegistrationBean<MdcFilter> {\n        val registrationBean = FilterRegistrationBean<MdcFilter>()\n        registrationBean.filter = mdcFilter\n        registrationBean.order = Int.MIN_VALUE\n        return registrationBean\n    }\n\n    @Bean\n    fun setResourceUrlEncodingFilter(): FilterRegistrationBean<ResourceUrlEncodingFilter> {\n        val registrationBean = FilterRegistrationBean<ResourceUrlEncodingFilter>()\n        registrationBean.filter = ResourceUrlEncodingFilter()\n        registrationBean.order = Int.MAX_VALUE - 2\n        return registrationBean\n    }\n\n    @Bean\n    fun setForwardedHeaderFilterOrder(): FilterRegistrationBean<ForwardedHeaderFilter> {\n        val registrationBean = FilterRegistrationBean<ForwardedHeaderFilter>()\n        registrationBean.filter = forwardedHeaderFilter\n        registrationBean.order = Int.MAX_VALUE - 1\n        return registrationBean\n    }\n\n    @Bean\n    fun setHttpContentCacheFilterOrder(): FilterRegistrationBean<HttpContentCacheFilter> {\n        val registrationBean = FilterRegistrationBean<HttpContentCacheFilter>()\n        registrationBean.filter = httpContentCacheFilter\n        registrationBean.order = Int.MAX_VALUE\n        return registrationBean\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/SwaggerConfig.kt",
    "content": "package band.gosrock.api.config\n\nimport band.gosrock.common.annotation.ApiErrorCodeExample\nimport band.gosrock.common.annotation.ApiErrorExceptionsExample\nimport band.gosrock.common.annotation.DisableSwaggerSecurity\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.dto.ErrorReason\nimport band.gosrock.common.dto.ErrorResponse\nimport band.gosrock.common.exception.BaseErrorCode\nimport band.gosrock.common.exception.DuDoongCodeException\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport io.swagger.v3.core.jackson.ModelResolver\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport io.swagger.v3.oas.models.Components\nimport io.swagger.v3.oas.models.OpenAPI\nimport io.swagger.v3.oas.models.Operation\nimport io.swagger.v3.oas.models.examples.Example\nimport io.swagger.v3.oas.models.info.Info\nimport io.swagger.v3.oas.models.info.License\nimport io.swagger.v3.oas.models.media.Content\nimport io.swagger.v3.oas.models.media.MediaType\nimport io.swagger.v3.oas.models.responses.ApiResponse\nimport io.swagger.v3.oas.models.responses.ApiResponses\nimport io.swagger.v3.oas.models.security.SecurityScheme\nimport io.swagger.v3.oas.models.servers.Server\nimport org.springdoc.core.customizers.OperationCustomizer\nimport org.springframework.context.ApplicationContext\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\nimport org.springframework.web.method.HandlerMethod\nimport java.util.Collections\nimport jakarta.servlet.ServletContext\n\n/** Swagger 사용 환경을 위한 설정 파일 */\n@Configuration\n@org.springframework.context.annotation.Profile(\"!staging\", \"!prod\")\nclass SwaggerConfig(\n    private val applicationContext: ApplicationContext,\n) {\n    @Bean\n    fun openAPI(servletContext: ServletContext): OpenAPI {\n        val contextPath = servletContext.contextPath\n        val server = Server().url(contextPath)\n        return OpenAPI().servers(listOf(server)).components(authSetting()).info(swaggerInfo())\n    }\n\n    private fun swaggerInfo(): Info {\n        val license = License()\n        license.url = \"https://github.com/Gosrock/DuDoong-Backend\"\n        license.name = \"두둥\"\n        return Info()\n            .version(\"v0.0.1\")\n            .title(\"\\\"두둥 서버 API문서\\\"\")\n            .description(\"두둥 서버의 API 문서 입니다.\")\n            .license(license)\n    }\n\n    private fun authSetting(): Components =\n        Components()\n            .addSecuritySchemes(\n                \"access-token\",\n                SecurityScheme()\n                    .type(SecurityScheme.Type.HTTP)\n                    .scheme(\"bearer\")\n                    .bearerFormat(\"JWT\")\n                    .`in`(SecurityScheme.In.HEADER)\n                    .name(\"Authorization\"),\n            )\n            .addSecuritySchemes(\n                \"admin-token\",\n                SecurityScheme()\n                    .type(SecurityScheme.Type.APIKEY)\n                    .`in`(SecurityScheme.In.HEADER)\n                    .name(\"X-Admin-Token\")\n                    .description(\"Admin JWT 토큰 (aud:admin 포함)\"),\n            )\n\n    @Bean\n    fun modelResolver(objectMapper: ObjectMapper): ModelResolver = ModelResolver(objectMapper)\n\n    @Bean\n    fun customize(): OperationCustomizer =\n        OperationCustomizer { operation: Operation, handlerMethod: HandlerMethod ->\n            val methodAnnotation = handlerMethod.getMethodAnnotation(DisableSwaggerSecurity::class.java)\n            val apiErrorExceptionsExample = handlerMethod.getMethodAnnotation(ApiErrorExceptionsExample::class.java)\n            val apiErrorCodeExample = handlerMethod.getMethodAnnotation(ApiErrorCodeExample::class.java)\n\n\n            val tags = getTags(handlerMethod)\n            // DisableSecurity 어노테이션있을시 스웨거 시큐리티 설정 삭제\n            if (methodAnnotation != null) {\n                operation.security = Collections.emptyList()\n            }\n            // 태그 중복 설정시 제일 구체적인 값만 태그로 설정\n            if (tags.isNotEmpty()) {\n                operation.tags = Collections.singletonList(tags[0])\n            }\n            // ApiErrorExceptionsExample 어노테이션 단 메소드 적용\n            if (apiErrorExceptionsExample != null) {\n                generateExceptionResponseExample(operation, apiErrorExceptionsExample.value.java)\n            }\n            // ApiErrorCodeExample 어노테이션 단 메소드 적용\n            if (apiErrorCodeExample != null) {\n                generateErrorCodeResponseExample(operation, apiErrorCodeExample.value.java)\n            }\n            operation\n        }\n\n    /**\n     * BaseErrorCode 타입의 이넘값들을 문서화 시킵니다. ExplainError 어노테이션으로 부가설명을 붙일수있습니다.\n     * 필드들을 가져와서 예시 에러 객체를 동적으로 생성해서 예시값으로 붙입니다.\n     */\n    private fun generateErrorCodeResponseExample(\n        operation: Operation,\n        type: Class<out BaseErrorCode>,\n    ) {\n        val responses = operation.responses\n        val errorCodes = type.enumConstants\n\n        val statusWithExampleHolders = errorCodes\n            .map { baseErrorCode ->\n                val errorReason = baseErrorCode.getErrorReason()\n                ExampleHolder(\n                    holder = getSwaggerExample(baseErrorCode.getExplainError(), errorReason),\n                    code = errorReason.status,\n                    name = errorReason.code,\n                )\n            }\n            .groupBy { it.code }\n\n        addExamplesToResponses(responses, statusWithExampleHolders)\n    }\n\n    /**\n     * SwaggerExampleExceptions 타입의 클래스를 문서화 시킵니다.\n     * SwaggerExampleExceptions 타입의 클래스는 필드로 DuDoongCodeException 타입을 가지며,\n     * DuDoongCodeException 의 errorReason 와, ExplainError 의 설명을 문서화시킵니다.\n     */\n    private fun generateExceptionResponseExample(operation: Operation, type: Class<*>) {\n        val responses = operation.responses\n        val bean = applicationContext.getBean(type)\n        val declaredFields = bean.javaClass.declaredFields\n\n        val statusWithExampleHolders = declaredFields\n            .filter { field -> field.getAnnotation(ExplainError::class.java) != null }\n            .filter { field -> field.type == DuDoongCodeException::class.java }\n            .map { field ->\n                val exception = field.get(bean) as DuDoongCodeException\n                val annotation = field.getAnnotation(ExplainError::class.java)\n                val value = annotation.value\n                val errorReason = exception.getErrorReason()\n                ExampleHolder(\n                    holder = getSwaggerExample(value, errorReason),\n                    code = errorReason.status,\n                    name = field.name,\n                )\n            }\n            .groupBy { it.code }\n\n        addExamplesToResponses(responses, statusWithExampleHolders)\n    }\n\n    private fun getSwaggerExample(value: String, errorReason: ErrorReason): Example {\n        val errorResponse = ErrorResponse(errorReason, \"요청시 패스정보입니다.\")\n        val example = Example()\n        example.description(value)\n        example.value = errorResponse\n        return example\n    }\n\n    private fun addExamplesToResponses(\n        responses: ApiResponses,\n        statusWithExampleHolders: Map<Int, List<ExampleHolder>>,\n    ) {\n        statusWithExampleHolders.forEach { (status, exampleHolders) ->\n            val content = Content()\n            val mediaType = MediaType()\n            val apiResponse = ApiResponse()\n            exampleHolders.forEach { exampleHolder ->\n                mediaType.addExamples(exampleHolder.name, exampleHolder.holder)\n            }\n            content.addMediaType(\"application/json\", mediaType)\n            apiResponse.content = content\n            responses.addApiResponse(status.toString(), apiResponse)\n        }\n    }\n\n    companion object {\n        private fun getTags(handlerMethod: HandlerMethod): List<String> {\n            val tags = mutableListOf<String>()\n            val methodTags = handlerMethod.method.getAnnotationsByType(Tag::class.java)\n            val methodTagStrings = methodTags.map { it.name }\n            val classTags = handlerMethod.javaClass.getAnnotationsByType(Tag::class.java)\n            val classTagStrings = classTags.map { it.name }\n            tags.addAll(methodTagStrings)\n            tags.addAll(classTagStrings)\n            return tags\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/WebMvcConfig.kt",
    "content": "package band.gosrock.api.config\n\nimport band.gosrock.api.config.security.CurrentUserIdResolver\nimport org.springframework.context.annotation.Configuration\nimport org.springframework.web.method.support.HandlerMethodArgumentResolver\nimport org.springframework.web.servlet.config.annotation.PathMatchConfigurer\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer\n\n@Configuration\nclass WebMvcConfig(\n    private val currentUserIdResolver: CurrentUserIdResolver,\n) : WebMvcConfigurer {\n\n    override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {\n        resolvers.add(currentUserIdResolver)\n    }\n\n    override fun configurePathMatch(configurer: PathMatchConfigurer) {\n        configurer.setUseTrailingSlashMatch(true)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/rateLimit/IPRateLimiter.kt",
    "content": "package band.gosrock.api.config.rateLimit\n\nimport io.github.bucket4j.Bandwidth\nimport io.github.bucket4j.Bucket\nimport io.github.bucket4j.BucketConfiguration\nimport io.github.bucket4j.Refill\nimport io.github.bucket4j.distributed.proxy.ProxyManager\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.stereotype.Component\nimport java.time.Duration\nimport java.util.function.Supplier\n\n@Component\nclass IPRateLimiter(\n    private val buckets: ProxyManager<String>,\n) {\n    @Value(\"\\${throttle.overdraft}\")\n    private var overdraft: Long = 0\n\n    @Value(\"\\${throttle.greedyRefill}\")\n    private var greedyRefill: Long = 0\n\n    fun resolveBucket(key: String): Bucket {\n        val configSupplier = getConfigSupplierForUser()\n        return buckets.builder().build(key, configSupplier)\n    }\n\n    private fun getConfigSupplierForUser(): Supplier<BucketConfiguration> {\n        val refill = Refill.greedy(greedyRefill, Duration.ofMinutes(1))\n        val limit = Bandwidth.classic(overdraft, refill)\n        return Supplier { BucketConfiguration.builder().addLimit(limit).build() }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/rateLimit/ThrottlingInterceptor.kt",
    "content": "package band.gosrock.api.config.rateLimit\n\nimport band.gosrock.api.config.security.SecurityUtils\nimport band.gosrock.api.slack.sender.SlackThrottleErrorSender\nimport band.gosrock.common.dto.ErrorResponse\nimport band.gosrock.common.exception.GlobalErrorCode\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport org.slf4j.LoggerFactory\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.http.MediaType\nimport org.springframework.stereotype.Component\nimport org.springframework.web.servlet.HandlerInterceptor\nimport org.springframework.web.util.ContentCachingRequestWrapper\nimport jakarta.servlet.http.HttpServletRequest\nimport jakarta.servlet.http.HttpServletResponse\n\nprivate val log = LoggerFactory.getLogger(ThrottlingInterceptor::class.java)\n\n@Component\nclass ThrottlingInterceptor(\n    private val userRateLimiter: UserRateLimiter,\n    private val ipRateLimiter: IPRateLimiter,\n    private val objectMapper: ObjectMapper,\n    private val slackThrottleErrorSender: SlackThrottleErrorSender,\n) : HandlerInterceptor {\n\n    @Value(\"\\${acl.whiteList}\")\n    private lateinit var aclWhiteList: List<String>\n\n    override fun preHandle(\n        request: HttpServletRequest,\n        response: HttpServletResponse,\n        handler: Any,\n    ): Boolean {\n        val userId = SecurityUtils.getCurrentUserId()\n        val remoteAddr = request.remoteAddr\n        log.info(\"remoteAddr : $remoteAddr\")\n\n        // next js ssr 대응\n        if (aclWhiteList.contains(remoteAddr)) {\n            log.info(\"white List pass$remoteAddr\")\n            return true\n        }\n\n        val bucket = if (userId == 0L) {\n            // 익명 유저 ip 기반처리\n            ipRateLimiter.resolveBucket(remoteAddr)\n        } else {\n            // 비 익명 유저 유저 아이디 기반 처리\n            userRateLimiter.resolveBucket(userId.toString())\n        }\n\n        val availableTokens = bucket.availableTokens\n        log.info(\"$userId : $availableTokens\")\n\n        if (bucket.tryConsume(1)) {\n            return true\n        }\n\n        // 슬랙 알림 메시지 발송.\n        // limit is exceeded\n        val cachingRequest = request as ContentCachingRequestWrapper\n        slackThrottleErrorSender.execute(cachingRequest, userId)\n        responseTooManyRequestError(request, response)\n\n        return false\n    }\n\n    private fun responseTooManyRequestError(\n        request: HttpServletRequest,\n        response: HttpServletResponse,\n    ) {\n        val errorResponse = ErrorResponse(\n            GlobalErrorCode.TOO_MANY_REQUEST.getErrorReason(),\n            request.requestURL.toString(),\n        )\n        response.characterEncoding = \"UTF-8\"\n        response.contentType = MediaType.APPLICATION_JSON_VALUE\n        response.status = errorResponse.status\n        response.writer.write(objectMapper.writeValueAsString(errorResponse))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/rateLimit/ThrottlingWebConfigure.kt",
    "content": "package band.gosrock.api.config.rateLimit\n\nimport org.springframework.stereotype.Component\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer\n\n@Component\nclass ThrottlingWebConfigure(\n    private val throttlingInterceptor: ThrottlingInterceptor,\n) : WebMvcConfigurer {\n    override fun addInterceptors(registry: InterceptorRegistry) {\n        registry.addInterceptor(throttlingInterceptor)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/rateLimit/UserRateLimiter.kt",
    "content": "package band.gosrock.api.config.rateLimit\n\nimport io.github.bucket4j.Bandwidth\nimport io.github.bucket4j.Bucket\nimport io.github.bucket4j.BucketConfiguration\nimport io.github.bucket4j.Refill\nimport io.github.bucket4j.distributed.proxy.ProxyManager\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.stereotype.Component\nimport java.time.Duration\nimport java.util.function.Supplier\n\n@Component\nclass UserRateLimiter(\n    private val buckets: ProxyManager<String>,\n) {\n    @Value(\"\\${throttle.overdraft}\")\n    private var overdraft: Long = 0\n\n    @Value(\"\\${throttle.greedyRefill}\")\n    private var greedyRefill: Long = 0\n\n    fun resolveBucket(key: String): Bucket {\n        val configSupplier = getConfigSupplierForUser()\n        return buckets.builder().build(key, configSupplier)\n    }\n\n    private fun getConfigSupplierForUser(): Supplier<BucketConfiguration> {\n        val refill = Refill.greedy(greedyRefill, Duration.ofMinutes(1))\n        val limit = Bandwidth.classic(overdraft, refill)\n        return Supplier { BucketConfiguration.builder().addLimit(limit).build() }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/response/GlobalExceptionHandler.kt",
    "content": "package band.gosrock.api.config.response\n\nimport band.gosrock.api.config.security.SecurityUtils\nimport band.gosrock.api.slack.sender.SlackInternalErrorSender\nimport band.gosrock.common.dto.ErrorReason\nimport band.gosrock.common.dto.ErrorResponse\nimport band.gosrock.common.exception.BaseErrorCode\nimport band.gosrock.common.exception.DuDoongCodeException\nimport band.gosrock.common.exception.DuDoongDynamicException\nimport band.gosrock.common.exception.GlobalErrorCode\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport org.slf4j.LoggerFactory\nimport org.springframework.http.HttpHeaders\nimport org.springframework.http.HttpStatus\nimport org.springframework.http.HttpStatusCode\nimport org.springframework.http.ResponseEntity\nimport org.springframework.http.server.ServletServerHttpRequest\nimport org.springframework.validation.FieldError\nimport org.springframework.web.bind.MethodArgumentNotValidException\nimport org.springframework.web.bind.annotation.ExceptionHandler\nimport org.springframework.web.bind.annotation.RestControllerAdvice\nimport org.springframework.web.context.request.ServletWebRequest\nimport org.springframework.web.context.request.WebRequest\nimport org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler\nimport org.springframework.web.util.ContentCachingRequestWrapper\nimport org.springframework.web.util.UriComponentsBuilder\nimport jakarta.servlet.http.HttpServletRequest\nimport jakarta.validation.ConstraintViolationException\n\nprivate val log = LoggerFactory.getLogger(GlobalExceptionHandler::class.java)\n\n@RestControllerAdvice\nclass GlobalExceptionHandler(\n    private val slackInternalErrorSender: SlackInternalErrorSender,\n) : ResponseEntityExceptionHandler() {\n\n    override fun handleExceptionInternal(\n        ex: Exception,\n        body: Any?,\n        headers: HttpHeaders,\n        statusCode: HttpStatusCode,\n        request: WebRequest,\n    ): ResponseEntity<Any>? {\n        val servletWebRequest = request as ServletWebRequest\n        val url = UriComponentsBuilder\n            .fromHttpRequest(ServletServerHttpRequest(servletWebRequest.request))\n            .build()\n            .toUriString()\n        val status = HttpStatus.valueOf(statusCode.value())\n        val errorResponse = ErrorResponse(status.value(), status.name, ex.message ?: \"\", url)\n        return super.handleExceptionInternal(ex, errorResponse, headers, statusCode, request)\n    }\n\n    override fun handleMethodArgumentNotValid(\n        ex: MethodArgumentNotValidException,\n        headers: HttpHeaders,\n        statusCode: HttpStatusCode,\n        request: WebRequest,\n    ): ResponseEntity<Any>? {\n        val errors: List<FieldError> = ex.bindingResult.fieldErrors\n        val servletWebRequest = request as ServletWebRequest\n        val url = UriComponentsBuilder\n            .fromHttpRequest(ServletServerHttpRequest(servletWebRequest.request))\n            .build()\n            .toUriString()\n        val fieldAndErrorMessages = errors.associate { it.field to it.defaultMessage }\n        val errorsToJsonString = ObjectMapper().writeValueAsString(fieldAndErrorMessages)\n        val status = HttpStatus.valueOf(statusCode.value())\n        val errorResponse = ErrorResponse(status.value(), status.name, errorsToJsonString, url)\n        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse)\n    }\n\n    @ExceptionHandler(DuDoongCodeException::class)\n    fun duDoongCodeExceptionHandler(\n        e: DuDoongCodeException,\n        request: HttpServletRequest,\n    ): ResponseEntity<ErrorResponse> {\n        val code: BaseErrorCode = e.errorCode\n        val errorReason: ErrorReason = code.getErrorReason()\n        val errorResponse = ErrorResponse(errorReason, request.requestURL.toString())\n        return ResponseEntity.status(HttpStatus.valueOf(errorReason.status)).body(errorResponse)\n    }\n\n    /** Request Param Validation 예외 처리 */\n    @ExceptionHandler(ConstraintViolationException::class)\n    fun constraintViolationExceptionHandler(\n        e: ConstraintViolationException,\n        request: HttpServletRequest,\n    ): ResponseEntity<ErrorResponse> {\n        val bindingErrors = mutableMapOf<String, Any?>()\n        e.constraintViolations.forEach { constraintViolation ->\n            val propertyPath = constraintViolation.propertyPath.toString().split(\".\")\n            val path = propertyPath.drop(propertyPath.size - 1).firstOrNull()\n            bindingErrors[path ?: \"\"] = constraintViolation.message\n        }\n        val errorReason = ErrorReason(\n            status = 400,\n            code = \"BAD_REQUEST\",\n            reason = bindingErrors.toString(),\n        )\n        val errorResponse = ErrorResponse(errorReason, request.requestURL.toString())\n        return ResponseEntity.status(HttpStatus.valueOf(errorReason.status)).body(errorResponse)\n    }\n\n    @ExceptionHandler(DuDoongDynamicException::class)\n    fun duDoongDynamicExceptionHandler(\n        e: DuDoongDynamicException,\n        request: HttpServletRequest,\n    ): ResponseEntity<ErrorResponse> {\n        val errorResponse = ErrorResponse(\n            e.status,\n            e.code,\n            e.reason,\n            request.requestURL.toString(),\n        )\n        return ResponseEntity.status(HttpStatus.valueOf(e.status)).body(errorResponse)\n    }\n\n    @ExceptionHandler(Exception::class)\n    fun handleException(e: Exception, request: HttpServletRequest): ResponseEntity<ErrorResponse> {\n        val cachingRequest = request as ContentCachingRequestWrapper\n        val userId = SecurityUtils.getCurrentUserId()\n        val url = UriComponentsBuilder\n            .fromHttpRequest(ServletServerHttpRequest(request))\n            .build()\n            .toUriString()\n\n        log.error(\"INTERNAL_SERVER_ERROR\", e)\n        val internalServerError = GlobalErrorCode.INTERNAL_SERVER_ERROR\n        val errorResponse = ErrorResponse(\n            internalServerError.status,\n            internalServerError.code,\n            internalServerError.reason,\n            url,\n        )\n\n        slackInternalErrorSender.execute(cachingRequest, e, userId)\n        return ResponseEntity.status(HttpStatus.valueOf(internalServerError.status)).body(errorResponse)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/response/SuccessResponseAdvice.kt",
    "content": "package band.gosrock.api.config.response\n\nimport band.gosrock.common.dto.SuccessResponse\nimport org.springframework.core.MethodParameter\nimport org.springframework.http.HttpStatus\nimport org.springframework.http.MediaType\nimport org.springframework.http.converter.HttpMessageConverter\nimport org.springframework.http.server.ServerHttpRequest\nimport org.springframework.http.server.ServerHttpResponse\nimport org.springframework.http.server.ServletServerHttpResponse\nimport org.springframework.web.bind.annotation.RestControllerAdvice\nimport org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice\nimport jakarta.servlet.http.HttpServletResponse\n\n@RestControllerAdvice(basePackages = [\"band.gosrock\"])\nclass SuccessResponseAdvice : ResponseBodyAdvice<Any> {\n\n    override fun supports(\n        returnType: MethodParameter,\n        converterType: Class<out HttpMessageConverter<*>>,\n    ): Boolean = true\n\n    override fun beforeBodyWrite(\n        body: Any?,\n        returnType: MethodParameter,\n        selectedContentType: MediaType,\n        selectedConverterType: Class<out HttpMessageConverter<*>>,\n        request: ServerHttpRequest,\n        response: ServerHttpResponse,\n    ): Any? {\n        val servletResponse: HttpServletResponse =\n            (response as ServletServerHttpResponse).servletResponse\n\n        val status = servletResponse.status\n        val resolve = HttpStatus.resolve(status) ?: return body\n\n        if (body is ByteArray || selectedContentType == MediaType.APPLICATION_OCTET_STREAM) {\n            return body\n        }\n\n        return if (resolve.is2xxSuccessful) {\n            SuccessResponse(status, body)\n        } else {\n            body\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/security/AccessDeniedFilter.kt",
    "content": "package band.gosrock.api.config.security\n\nimport band.gosrock.common.consts.DuDoongStatic.SwaggerPatterns\nimport band.gosrock.common.dto.ErrorResponse\nimport band.gosrock.common.exception.BaseErrorCode\nimport band.gosrock.common.exception.DuDoongCodeException\nimport band.gosrock.common.exception.GlobalErrorCode\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport jakarta.servlet.FilterChain\nimport jakarta.servlet.http.HttpServletRequest\nimport jakarta.servlet.http.HttpServletResponse\nimport org.springframework.http.MediaType\nimport org.springframework.security.access.AccessDeniedException\nimport org.springframework.stereotype.Component\nimport org.springframework.util.PatternMatchUtils\nimport org.springframework.web.filter.OncePerRequestFilter\n\n@Component\nclass AccessDeniedFilter(\n    private val objectMapper: ObjectMapper\n) : OncePerRequestFilter() {\n\n    override fun shouldNotFilter(request: HttpServletRequest): Boolean {\n        val servletPath = request.servletPath\n        return PatternMatchUtils.simpleMatch(SwaggerPatterns, servletPath)\n    }\n\n    override fun doFilterInternal(\n        request: HttpServletRequest,\n        response: HttpServletResponse,\n        filterChain: FilterChain\n    ) {\n        try {\n            filterChain.doFilter(request, response)\n        } catch (e: DuDoongCodeException) {\n            responseToClient(\n                response,\n                getErrorResponse(e.errorCode, request.requestURL.toString())\n            )\n        } catch (e: AccessDeniedException) {\n            val accessDenied = ErrorResponse(\n                GlobalErrorCode.ACCESS_TOKEN_NOT_EXIST.getErrorReason(),\n                request.requestURL.toString()\n            )\n            responseToClient(response, accessDenied)\n        }\n    }\n\n    private fun getErrorResponse(errorCode: BaseErrorCode, path: String): ErrorResponse {\n        val errorReason = errorCode.getErrorReason()\n        return ErrorResponse(errorReason.status, errorReason.code, errorReason.reason, path)\n    }\n\n    private fun responseToClient(response: HttpServletResponse, errorResponse: ErrorResponse) {\n        response.characterEncoding = \"UTF-8\"\n        response.contentType = MediaType.APPLICATION_JSON_VALUE\n        response.status = errorResponse.status\n        response.writer.write(objectMapper.writeValueAsString(errorResponse))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/security/AuthDetails.kt",
    "content": "package band.gosrock.api.config.security\n\nimport org.springframework.security.core.GrantedAuthority\nimport org.springframework.security.core.authority.SimpleGrantedAuthority\nimport org.springframework.security.core.userdetails.UserDetails\n\nclass AuthDetails(\n    private val userId: String,\n    private val role: String,\n) : UserDetails {\n\n    override fun getAuthorities(): Collection<GrantedAuthority> =\n        setOf(SimpleGrantedAuthority(\"ROLE_$role\"))\n\n    override fun getPassword(): String? = null\n\n    override fun getUsername(): String = userId\n\n    override fun isAccountNonExpired(): Boolean = true\n\n    override fun isAccountNonLocked(): Boolean = true\n\n    override fun isCredentialsNonExpired(): Boolean = true\n\n    override fun isEnabled(): Boolean = true\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/security/CorsConfig.kt",
    "content": "package band.gosrock.api.config.security\n\nimport band.gosrock.common.helper.SpringEnvironmentHelper\nimport org.springframework.context.annotation.Configuration\nimport org.springframework.web.servlet.config.annotation.CorsRegistry\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer\n\n@Configuration\nclass CorsConfig(\n    private val springEnvironmentHelper: SpringEnvironmentHelper\n) : WebMvcConfigurer {\n\n    override fun addCorsMappings(registry: CorsRegistry) {\n        val allowedOriginPatterns = mutableListOf(\n            \"https://dudoong.com\",\n            \"https://staging.dudoong.com\",\n            \"https://internal-admin.dudoong.com\",\n            \"https://staging-internal-admin.dudoong.com\",\n            \"http://localhost:3000\",\n            \"http://localhost:5173\"\n        )\n        registry.addMapping(\"/**\")\n            .allowedMethods(\"*\")\n            .allowedOriginPatterns(*allowedOriginPatterns.toTypedArray())\n            .exposedHeaders(\"Set-Cookie\")\n            .allowCredentials(true)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/security/CurrentUserIdResolver.kt",
    "content": "package band.gosrock.api.config.security\n\nimport band.gosrock.common.annotation.CurrentUserId\nimport org.springframework.core.MethodParameter\nimport org.springframework.stereotype.Component\nimport org.springframework.web.bind.support.WebDataBinderFactory\nimport org.springframework.web.context.request.NativeWebRequest\nimport org.springframework.web.method.support.HandlerMethodArgumentResolver\nimport org.springframework.web.method.support.ModelAndViewContainer\n\n@Component\nclass CurrentUserIdResolver : HandlerMethodArgumentResolver {\n\n    override fun supportsParameter(parameter: MethodParameter): Boolean =\n        parameter.hasParameterAnnotation(CurrentUserId::class.java) &&\n            parameter.parameterType == Long::class.java\n\n    override fun resolveArgument(\n        parameter: MethodParameter,\n        mavContainer: ModelAndViewContainer?,\n        webRequest: NativeWebRequest,\n        binderFactory: WebDataBinderFactory?,\n    ): Long = SecurityUtils.getCurrentUserId()\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/security/JwtExceptionFilter.kt",
    "content": "package band.gosrock.api.config.security\n\nimport band.gosrock.common.dto.ErrorResponse\nimport band.gosrock.common.exception.BaseErrorCode\nimport band.gosrock.common.exception.DuDoongCodeException\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport jakarta.servlet.FilterChain\nimport jakarta.servlet.http.HttpServletRequest\nimport jakarta.servlet.http.HttpServletResponse\nimport org.springframework.http.MediaType\nimport org.springframework.stereotype.Component\nimport org.springframework.web.filter.OncePerRequestFilter\n\n@Component\nclass JwtExceptionFilter(\n    private val objectMapper: ObjectMapper\n) : OncePerRequestFilter() {\n\n    override fun doFilterInternal(\n        request: HttpServletRequest,\n        response: HttpServletResponse,\n        filterChain: FilterChain\n    ) {\n        try {\n            filterChain.doFilter(request, response)\n        } catch (e: DuDoongCodeException) {\n            responseToClient(\n                response,\n                getErrorResponse(e.errorCode, request.requestURL.toString())\n            )\n        }\n    }\n\n    private fun getErrorResponse(errorCode: BaseErrorCode, path: String): ErrorResponse =\n        ErrorResponse(errorCode.getErrorReason(), path)\n\n    private fun responseToClient(response: HttpServletResponse, errorResponse: ErrorResponse) {\n        response.characterEncoding = \"UTF-8\"\n        response.contentType = MediaType.APPLICATION_JSON_VALUE\n        response.status = errorResponse.status\n        response.writer.write(objectMapper.writeValueAsString(errorResponse))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/security/JwtTokenFilter.kt",
    "content": "package band.gosrock.api.config.security\n\nimport band.gosrock.api.auth.service.helper.CookieHelper\nimport band.gosrock.common.consts.DuDoongStatic\nimport band.gosrock.common.jwt.JwtTokenProvider\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport jakarta.servlet.FilterChain\nimport jakarta.servlet.http.HttpServletRequest\nimport jakarta.servlet.http.HttpServletResponse\nimport org.springframework.security.authentication.UsernamePasswordAuthenticationToken\nimport org.springframework.security.core.Authentication\nimport org.springframework.security.core.context.SecurityContextHolder\nimport org.springframework.stereotype.Component\nimport org.springframework.web.filter.OncePerRequestFilter\nimport org.springframework.web.util.WebUtils\n\n@Component\nclass JwtTokenFilter(\n    private val jwtTokenProvider: JwtTokenProvider,\n    private val userAdaptor: UserAdaptor,\n    private val cookieHelper: CookieHelper,\n) : OncePerRequestFilter() {\n\n    override fun doFilterInternal(\n        request: HttpServletRequest,\n        response: HttpServletResponse,\n        filterChain: FilterChain\n    ) {\n        val token = resolveToken(request)\n\n        if (token != null) {\n            val authentication = getAuthentication(token)\n            SecurityContextHolder.getContext().authentication = authentication\n        }\n\n        filterChain.doFilter(request, response)\n    }\n\n    private fun resolveToken(request: HttpServletRequest): String? {\n        // Admin 전용 헤더 우선\n        val adminToken = request.getHeader(DuDoongStatic.ADMIN_TOKEN_HEADER)\n        if (adminToken != null) {\n            return adminToken\n        }\n        // 쿠키방식 지원\n        val accessTokenCookie = WebUtils.getCookie(request, cookieHelper.getAccessTokenName())\n        if (accessTokenCookie != null) {\n            return accessTokenCookie.value\n        }\n        // 기존 jwt 방식 지원\n        val rawHeader = request.getHeader(DuDoongStatic.AUTH_HEADER) ?: return null\n\n        if (rawHeader.length > DuDoongStatic.BEARER.length &&\n            rawHeader.startsWith(DuDoongStatic.BEARER)\n        ) {\n            return rawHeader.substring(DuDoongStatic.BEARER.length)\n        }\n        return null\n    }\n\n    fun getAuthentication(token: String): Authentication {\n        val accessTokenInfo = jwtTokenProvider.parseAccessToken(token)\n        val userId = accessTokenInfo.userId\n\n        // 매 요청마다 DB에서 실시간 role 조회 → 역할 변경 즉시 반영\n        val user = userAdaptor.queryUser(userId)\n        val role = user.accountRole.value\n\n        val userDetails = AuthDetails(userId.toString(), role)\n        return UsernamePasswordAuthenticationToken(userDetails, \"user\", userDetails.authorities)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/security/SecurityConfig.kt",
    "content": "package band.gosrock.api.config.security\n\nimport band.gosrock.common.dto.ErrorResponse\nimport band.gosrock.common.exception.GlobalErrorCode\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\nimport org.springframework.http.HttpMethod\nimport org.springframework.http.MediaType\nimport org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl\nimport org.springframework.security.config.annotation.web.builders.HttpSecurity\nimport org.springframework.security.config.annotation.web.configuration.EnableWebSecurity\nimport org.springframework.security.config.http.SessionCreationPolicy\nimport org.springframework.security.web.SecurityFilterChain\nimport org.springframework.security.web.authentication.www.BasicAuthenticationFilter\n\n@Configuration\n@EnableWebSecurity\nclass SecurityConfig(\n    private val jwtTokenFilter: JwtTokenFilter,\n    private val accessDeniedFilter: AccessDeniedFilter,\n    private val jwtExceptionFilter: JwtExceptionFilter,\n    private val objectMapper: ObjectMapper,\n) {\n\n    @Bean\n    fun filterChain(http: HttpSecurity): SecurityFilterChain {\n        http\n            .formLogin { it.disable() }\n            .cors {}\n            .csrf { it.disable() }\n            .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }\n\n        http.exceptionHandling { exceptions ->\n            exceptions.accessDeniedHandler { request, response, _ ->\n                val errorResponse = ErrorResponse(\n                    GlobalErrorCode.ACCESS_TOKEN_NOT_EXIST.getErrorReason(),\n                    request.requestURL.toString()\n                )\n                response.characterEncoding = \"UTF-8\"\n                response.contentType = MediaType.APPLICATION_JSON_VALUE\n                response.status = 403\n                response.writer.write(objectMapper.writeValueAsString(errorResponse))\n            }\n            exceptions.authenticationEntryPoint { request, response, _ ->\n                val errorResponse = ErrorResponse(\n                    GlobalErrorCode.ACCESS_TOKEN_NOT_EXIST.getErrorReason(),\n                    request.requestURL.toString()\n                )\n                response.characterEncoding = \"UTF-8\"\n                response.contentType = MediaType.APPLICATION_JSON_VALUE\n                response.status = 401\n                response.writer.write(objectMapper.writeValueAsString(errorResponse))\n            }\n        }\n\n        http.authorizeHttpRequests { auth ->\n            auth\n                .requestMatchers(\"/api/v1/auth/oauth/**\").permitAll()\n                .requestMatchers(\"/api/v1/auth/token/refresh\").permitAll()\n                .requestMatchers(HttpMethod.GET, \"/api/v1/events/{eventId:[0-9]*$}\").permitAll()\n                .requestMatchers(HttpMethod.GET, \"/api/v1/events/{eventId:[0-9]*$}/ticketItems\").permitAll()\n                .requestMatchers(HttpMethod.GET, \"/api/v1/events/{eventId:[0-9]*$}/comments/**\").permitAll()\n                .requestMatchers(HttpMethod.GET, \"/api/v1/events/search\").permitAll()\n                .requestMatchers(HttpMethod.GET, \"/api/v1/examples/health\").permitAll()\n                .requestMatchers(HttpMethod.POST, \"/api/v1/coupons/campaigns\").hasRole(\"SUPER_ADMIN\")\n                .requestMatchers(\"/internal-api/**\").hasAnyRole(\"ADMIN\", \"SUPER_ADMIN\")\n                .anyRequest().hasRole(\"USER\")\n        }\n\n        http.addFilterBefore(jwtTokenFilter, BasicAuthenticationFilter::class.java)\n        http.addFilterBefore(jwtExceptionFilter, JwtTokenFilter::class.java)\n        http.addFilterBefore(accessDeniedFilter, JwtTokenFilter::class.java)\n\n        return http.build()\n    }\n\n    @Bean\n    fun roleHierarchy(): RoleHierarchyImpl {\n        val roleHierarchy = RoleHierarchyImpl()\n        roleHierarchy.setHierarchy(\"ROLE_SUPER_ADMIN > ROLE_ADMIN > ROLE_USER\")\n        return roleHierarchy\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/config/security/SecurityUtils.kt",
    "content": "package band.gosrock.api.config.security\n\nimport band.gosrock.common.exception.SecurityContextNotFoundException\nimport org.springframework.security.core.authority.SimpleGrantedAuthority\nimport org.springframework.security.core.context.SecurityContextHolder\nimport org.springframework.util.CollectionUtils\n\nobject SecurityUtils {\n\n    private val anonymous = SimpleGrantedAuthority(\"ROLE_ANONYMOUS\")\n    private val swagger = SimpleGrantedAuthority(\"ROLE_SWAGGER\")\n    private val notUserAuthority = listOf(anonymous, swagger)\n\n    @JvmStatic\n    fun getCurrentUserId(): Long {\n        val authentication = SecurityContextHolder.getContext().authentication\n            ?: throw SecurityContextNotFoundException.EXCEPTION\n\n        if (authentication.isAuthenticated &&\n            !CollectionUtils.containsAny(authentication.authorities, notUserAuthority)\n        ) {\n            return authentication.name.toLong()\n        }\n        // 스웨거 유저일시 익명 유저 취급\n        // 익명유저시 userId 0 반환\n        return 0L\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/coupon/controller/CouponController.kt",
    "content": "package band.gosrock.api.coupon.controller\n\nimport band.gosrock.common.annotation.CurrentUserId\nimport band.gosrock.api.coupon.dto.reqeust.CreateCouponCampaignRequest\nimport band.gosrock.api.coupon.dto.response.CreateCouponCampaignResponse\nimport band.gosrock.api.coupon.dto.response.CreateUserCouponResponse\nimport band.gosrock.api.coupon.dto.response.ReadIssuedCouponResponse\nimport band.gosrock.api.coupon.service.CreateCouponUseCase\nimport band.gosrock.api.coupon.service.CreateUserCouponUseCase\nimport band.gosrock.api.coupon.service.ReadIssuedCouponUseCase\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport jakarta.validation.Valid\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestBody\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RequestParam\nimport org.springframework.web.bind.annotation.RestController\n\n@SecurityRequirement(name = \"access-token\")\n@Tag(name = \"a0. [쿠폰]\")\n@RestController\n@RequestMapping(\"/api/v1/coupons\")\nclass CouponController(\n    private val createCouponUseCase: CreateCouponUseCase,\n    private val createUserCouponUseCase: CreateUserCouponUseCase,\n    private val readIssuedCouponUseCase: ReadIssuedCouponUseCase,\n) {\n    @Operation(summary = \"쿠폰 캠페인 생성 API\")\n    @PostMapping(\"/campaigns\")\n    fun createCouponCampaign(\n        @CurrentUserId userId: Long,\n        @RequestBody @Valid createCouponCampaignRequest: CreateCouponCampaignRequest,\n    ): CreateCouponCampaignResponse {\n        return createCouponUseCase.execute(userId, createCouponCampaignRequest)\n    }\n\n    @Operation(summary = \"유저 쿠폰 발급 API\")\n    @PostMapping(\"/campaigns/{coupon_code}\")\n    fun createUserCoupon(\n        @CurrentUserId userId: Long,\n        @PathVariable(\"coupon_code\") couponCode: String,\n    ): CreateUserCouponResponse {\n        return createUserCouponUseCase.execute(userId, couponCode)\n    }\n\n    @Operation(summary = \"내 쿠폰 조회 API\")\n    @GetMapping(\"\")\n    fun getAllMyIssuedCoupons(\n        @CurrentUserId userId: Long,\n        @RequestParam(required = false, defaultValue = \"true\") expired: Boolean,\n    ): ReadIssuedCouponResponse {\n        return readIssuedCouponUseCase.execute(userId, expired)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/coupon/dto/reqeust/CreateCouponCampaignRequest.kt",
    "content": "package band.gosrock.api.coupon.dto.reqeust\n\nimport band.gosrock.common.annotation.DateFormat\nimport band.gosrock.domain.domains.coupon.domain.ApplyTarget\nimport band.gosrock.domain.domains.coupon.domain.DiscountType\nimport io.swagger.v3.oas.annotations.media.Schema\nimport java.time.LocalDateTime\nimport jakarta.validation.constraints.Future\nimport jakarta.validation.constraints.Min\nimport jakarta.validation.constraints.NotBlank\nimport jakarta.validation.constraints.NotNull\nimport jakarta.validation.constraints.Positive\n\ndata class CreateCouponCampaignRequest(\n    @field:NotNull(message = \"discountType을 입력해주세요.\")\n    val discountType: DiscountType,\n\n    @Schema(nullable = true, defaultValue = \"ALL\")\n    val applyTarget: ApplyTarget? = null,\n\n    @field:NotNull(message = \"validTerm을 입력해주세요.\")\n    @field:Positive(message = \"validTerm은 양수여야합니다.(유효기간)\")\n    val validTerm: Long,\n\n    @Schema(type = \"string\", pattern = \"yyyy.MM.dd HH:mm\", description = \"쿠폰 발행 시작 시간\")\n    @field:NotNull(message = \"startAt을 입력해주세요.\")\n    @field:DateFormat\n    val startAt: LocalDateTime,\n\n    @Schema(type = \"string\", pattern = \"yyyy.MM.dd HH:mm\", description = \"쿠폰 발행 마감 시간\")\n    @field:NotNull(message = \"endAt을 입력해주세요.\")\n    @field:Future(message = \"endAt은 값이 미래여야합니다.\")\n    @field:DateFormat\n    val endAt: LocalDateTime,\n\n    @field:NotNull(message = \"issuedAmount을 입력해주세요.\")\n    @field:Positive(message = \"issuedAmount는 양수여야합니다.\")\n    val issuedAmount: Long,\n\n    @field:NotNull(message = \"discountAmount을 입력해주세요.\")\n    @field:Positive(message = \"discountAmount은 양수여야합니다.\")\n    val discountAmount: Long,\n\n    @field:NotBlank(message = \"couponCode를 입력해주세요.\")\n    val couponCode: String,\n\n    @Schema(description = \"쿠폰 사용 가능한 최소 결제 금액(원단위, 10000원 이상부터 입력 가능)\")\n    @field:NotNull(message = \"minimumCost(원 단위)를 입력해주세요.\")\n    @field:Min(value = 10000, message = \"10000원 이상부터 입력 가능합니다.\")\n    val minimumCost: Long,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/coupon/dto/response/CreateCouponCampaignResponse.kt",
    "content": "package band.gosrock.api.coupon.dto.response\n\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class CreateCouponCampaignResponse(\n    @Schema(description = \"쿠폰 캠페인 ID\") val couponCampaignId: Long,\n    @Schema(description = \"쿠폰 코드\") val couponCode: String,\n    @Schema(description = \"생성한 쿠폰 총 매수\") val issuedAmount: Long,\n    @Schema(description = \"쿠폰 생성한 슈퍼 어드민 user ID\") val userId: Long,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/coupon/dto/response/CreateUserCouponResponse.kt",
    "content": "package band.gosrock.api.coupon.dto.response\n\nimport band.gosrock.common.annotation.DateFormat\nimport band.gosrock.domain.domains.coupon.domain.DiscountType\nimport io.swagger.v3.oas.annotations.media.Schema\nimport java.time.LocalDateTime\n\ndata class CreateUserCouponResponse(\n    @Schema(description = \"발급한 쿠폰 id\") val issuedCouponId: Long,\n    @Schema(description = \"쿠폰 캠페인 id\") val couponCampaignId: Long,\n    @Schema(description = \"쿠폰 코드\") val couponCode: String,\n    @Schema(type = \"string\", pattern = \"yyyy.MM.dd HH:mm\", description = \"쿠폰 유효 기간\")\n    @DateFormat\n    val validTerm: LocalDateTime,\n    @Schema(description = \"할인타입(정액,정률)\") val discountType: DiscountType,\n    @Schema(description = \"할인량\") val discountAmount: Long,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/coupon/dto/response/ReadIssuedCouponOrderResponse.kt",
    "content": "package band.gosrock.api.coupon.dto.response\n\nimport band.gosrock.domain.common.vo.IssuedCouponInfoVo\nimport band.gosrock.domain.domains.coupon.domain.IssuedCoupon\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class ReadIssuedCouponOrderResponse(\n    @Schema(description = \"총 개수\") val totalNum: Long,\n    @Schema(description = \"쿠폰 정보 리스트\") val issuedCouponInfoList: List<IssuedCouponInfoVo>,\n) {\n    companion object {\n        fun of(issuedCoupons: List<IssuedCoupon>): ReadIssuedCouponOrderResponse {\n            if (issuedCoupons.isEmpty()) {\n                return ReadIssuedCouponOrderResponse(totalNum = 0L, issuedCouponInfoList = emptyList())\n            }\n            return ReadIssuedCouponOrderResponse(\n                totalNum = issuedCoupons.size.toLong(),\n                issuedCouponInfoList = issuedCoupons.map { IssuedCouponInfoVo.of(it) },\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/coupon/dto/response/ReadIssuedCouponResponse.kt",
    "content": "package band.gosrock.api.coupon.dto.response\n\nimport band.gosrock.domain.common.vo.IssuedCouponInfoVo\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class ReadIssuedCouponResponse(\n    @Schema(description = \"사용 가능 쿠폰 총 개수\") val availableCouponNum: Long,\n    @Schema(description = \"사용 가능 쿠폰 정보 리스트\") val availableCouponInfoList: List<IssuedCouponInfoVo>,\n    @Schema(description = \"사용됨/기한만료 쿠폰 총 개수\") val expiredCouponNum: Long,\n    @Schema(description = \"사용됨/기한만료 쿠폰 정보 리스트\") val expiredCouponInfoList: List<IssuedCouponInfoVo>,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/coupon/mapper/CouponCampaignMapper.kt",
    "content": "package band.gosrock.api.coupon.mapper\n\nimport band.gosrock.api.coupon.dto.reqeust.CreateCouponCampaignRequest\nimport band.gosrock.api.coupon.dto.response.CreateCouponCampaignResponse\nimport band.gosrock.common.annotation.Mapper\nimport band.gosrock.domain.common.vo.DateTimePeriod\nimport band.gosrock.domain.domains.coupon.domain.CouponCampaign\nimport band.gosrock.domain.domains.coupon.domain.CouponStockInfo\nimport java.time.LocalDateTime\n\n@Mapper\nclass CouponCampaignMapper {\n\n    fun toEntity(createCouponCampaignRequest: CreateCouponCampaignRequest, userId: Long): CouponCampaign {\n        val couponStockInfo = toCouponStockInfo(createCouponCampaignRequest.issuedAmount)\n        val dateTimePeriod = toDateTimePeriod(createCouponCampaignRequest.startAt, createCouponCampaignRequest.endAt)\n\n        return CouponCampaign(\n            userId = userId,\n            discountType = createCouponCampaignRequest.discountType,\n            applyTarget = createCouponCampaignRequest.applyTarget,\n            validTerm = createCouponCampaignRequest.validTerm,\n            dateTimePeriod = dateTimePeriod,\n            couponStockInfo = couponStockInfo,\n            discountAmount = createCouponCampaignRequest.discountAmount,\n            couponCode = createCouponCampaignRequest.couponCode,\n            minimumCost = createCouponCampaignRequest.minimumCost,\n        )\n    }\n\n    companion object {\n        fun toCreateCouponCampaignResponse(couponCampaign: CouponCampaign, userId: Long): CreateCouponCampaignResponse {\n            return CreateCouponCampaignResponse(\n                couponCampaignId = couponCampaign.id!!,\n                couponCode = couponCampaign.couponCode!!,\n                issuedAmount = couponCampaign.couponStockInfo!!.issuedAmount!!,\n                userId = userId,\n            )\n        }\n\n        fun toCouponStockInfo(issuedAmount: Long): CouponStockInfo {\n            return CouponStockInfo(\n                issuedAmount = issuedAmount,\n                remainingAmount = issuedAmount,\n            )\n        }\n\n        fun toDateTimePeriod(startAt: LocalDateTime, endAt: LocalDateTime): DateTimePeriod {\n            return DateTimePeriod(startAt, endAt)\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/coupon/mapper/IssuedCouponMapper.kt",
    "content": "package band.gosrock.api.coupon.mapper\n\nimport band.gosrock.api.coupon.dto.response.CreateUserCouponResponse\nimport band.gosrock.api.coupon.dto.response.ReadIssuedCouponResponse\nimport band.gosrock.common.annotation.Mapper\nimport band.gosrock.domain.common.vo.IssuedCouponInfoVo\nimport band.gosrock.domain.domains.coupon.adaptor.IssuedCouponAdaptor\nimport band.gosrock.domain.domains.coupon.domain.CouponCampaign\nimport band.gosrock.domain.domains.coupon.domain.IssuedCoupon\n\n@Mapper\nclass IssuedCouponMapper(\n    private val issuedCouponAdaptor: IssuedCouponAdaptor,\n) {\n    fun toEntity(couponCampaign: CouponCampaign, userId: Long): IssuedCoupon {\n        return IssuedCoupon(couponCampaign = couponCampaign, userId = userId)\n    }\n\n    fun toCreateUserCouponResponse(issuedCoupon: IssuedCoupon, couponCampaign: CouponCampaign): CreateUserCouponResponse {\n        return CreateUserCouponResponse(\n            issuedCouponId = issuedCoupon.id!!,\n            couponCampaignId = couponCampaign.id!!,\n            couponCode = couponCampaign.couponCode!!,\n            validTerm = issuedCoupon.calculateValidTerm(),\n            discountType = couponCampaign.discountType!!,\n            discountAmount = couponCampaign.discountAmount!!,\n        )\n    }\n\n    fun toReadIssuedCouponMyPageResponse(\n        availableIssuedCoupons: List<IssuedCoupon>,\n        expiredIssuedCoupons: List<IssuedCoupon>,\n    ): ReadIssuedCouponResponse {\n        val available = availableIssuedCoupons.ifEmpty { emptyList() }\n        return ReadIssuedCouponResponse(\n            availableCouponNum = available.size.toLong(),\n            availableCouponInfoList = available.map { IssuedCouponInfoVo.of(it) },\n            expiredCouponNum = expiredIssuedCoupons.size.toLong(),\n            expiredCouponInfoList = expiredIssuedCoupons.map { IssuedCouponInfoVo.of(it) },\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/coupon/service/CreateCouponUseCase.kt",
    "content": "package band.gosrock.api.coupon.service\n\nimport band.gosrock.api.coupon.dto.reqeust.CreateCouponCampaignRequest\nimport band.gosrock.api.coupon.dto.response.CreateCouponCampaignResponse\nimport band.gosrock.api.coupon.mapper.CouponCampaignMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.coupon.service.CreateCouponCampaignDomainService\nimport band.gosrock.domain.domains.host.service.HostService\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass CreateCouponUseCase(\n    private val userAdaptor: UserAdaptor,\n    private val createCouponCampaignDomainService: CreateCouponCampaignDomainService,\n    private val hostService: HostService,\n    private val couponCampaignMapper: CouponCampaignMapper,\n) {\n    @Transactional\n    fun execute(userId: Long, createCouponCampaignRequest: CreateCouponCampaignRequest): CreateCouponCampaignResponse {\n        // 존재하는 유저인지 검증\n        val user = userAdaptor.queryUser(userId)\n        // 이미 생성된 쿠폰 코드인지 검증\n        createCouponCampaignDomainService.checkCouponCodeExists(createCouponCampaignRequest.couponCode)\n        // 쿠폰 생성\n        val couponCampaign = createCouponCampaignDomainService.createCouponCampaign(\n            couponCampaignMapper.toEntity(createCouponCampaignRequest, user.id!!),\n        )\n        return CouponCampaignMapper.toCreateCouponCampaignResponse(couponCampaign, user.id!!)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/coupon/service/CreateUserCouponUseCase.kt",
    "content": "package band.gosrock.api.coupon.service\n\nimport band.gosrock.api.coupon.dto.response.CreateUserCouponResponse\nimport band.gosrock.api.coupon.mapper.IssuedCouponMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.coupon.adaptor.CouponCampaignAdaptor\nimport band.gosrock.domain.domains.coupon.service.CreateIssuedCouponDomainService\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass CreateUserCouponUseCase(\n    private val userAdaptor: UserAdaptor,\n    private val issuedCouponMapper: IssuedCouponMapper,\n    private val couponCampaignAdaptor: CouponCampaignAdaptor,\n    private val createIssuedCouponDomainService: CreateIssuedCouponDomainService,\n) {\n    @Transactional\n    fun execute(userId: Long, couponCode: String): CreateUserCouponResponse {\n        // 존재하는 유저인지 검증\n        val user = userAdaptor.queryUser(userId)\n        // 쿠폰 코드 검증\n        val couponCampaign = couponCampaignAdaptor.findByCouponCode(couponCode)\n        // 재고 감소 및 쿠폰 발급\n        val issuedCoupon = createIssuedCouponDomainService.createIssuedCoupon(\n            issuedCouponMapper.toEntity(couponCampaign, user.id!!),\n            couponCampaign,\n        )\n        return issuedCouponMapper.toCreateUserCouponResponse(issuedCoupon, couponCampaign)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/coupon/service/ReadIssuedCouponUseCase.kt",
    "content": "package band.gosrock.api.coupon.service\n\nimport band.gosrock.api.coupon.dto.response.ReadIssuedCouponResponse\nimport band.gosrock.api.coupon.mapper.IssuedCouponMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.coupon.adaptor.IssuedCouponAdaptor\nimport band.gosrock.domain.domains.coupon.domain.IssuedCoupon\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass ReadIssuedCouponUseCase(\n    private val userAdaptor: UserAdaptor,\n    private val issuedCouponAdaptor: IssuedCouponAdaptor,\n    private val issuedCouponMapper: IssuedCouponMapper,\n) {\n    @Transactional(readOnly = true)\n    fun execute(userId: Long, expired: Boolean): ReadIssuedCouponResponse {\n        // 존재하는 유저인지 검증\n        val user = userAdaptor.queryUser(userId)\n\n        val issuedCoupons = issuedCouponAdaptor.findAllByUserId(user.id!!)\n        // 사용 가능 쿠폰 조회 (사용 이전, 유효 기간 이전)\n        val validTermAvailableIssuedCoupons = filterAvailableCouponList(issuedCoupons)\n\n        // 만료된 쿠폰 조회(사용 완료, 유효 기간 만료)\n        if (expired) {\n            val expiredValidTermIssuedCoupons = filterExpiredCouponList(issuedCoupons)\n            return issuedCouponMapper.toReadIssuedCouponMyPageResponse(\n                validTermAvailableIssuedCoupons,\n                expiredValidTermIssuedCoupons,\n            )\n        }\n\n        return issuedCouponMapper.toReadIssuedCouponMyPageResponse(\n            validTermAvailableIssuedCoupons,\n            emptyList(),\n        )\n    }\n\n    fun filterAvailableCouponList(issuedCoupons: List<IssuedCoupon>): List<IssuedCoupon> {\n        return issuedCoupons.filter { it.isAvailableTerm() && !it.usageStatus }\n    }\n\n    fun filterExpiredCouponList(issuedCoupons: List<IssuedCoupon>): List<IssuedCoupon> {\n        return issuedCoupons.filter { !it.isAvailableTerm() || it.usageStatus }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/dto/IssuedTicketMailDTO.kt",
    "content": "package band.gosrock.api.email.dto\n\nimport band.gosrock.infrastructure.config.mail.dto.EmailEventInfo\nimport band.gosrock.infrastructure.config.mail.dto.EmailIssuedTicketInfo\nimport band.gosrock.infrastructure.config.mail.dto.EmailUserInfo\n\ndata class IssuedTicketMailDTO(\n    val userInfo: EmailUserInfo,\n    val issuedTicketInfo: EmailIssuedTicketInfo,\n    val eventInfo: EmailEventInfo,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/dto/OrderMailDto.kt",
    "content": "package band.gosrock.api.email.dto\n\nimport band.gosrock.infrastructure.config.mail.dto.EmailEventInfo\nimport band.gosrock.infrastructure.config.mail.dto.EmailOrderInfo\nimport band.gosrock.infrastructure.config.mail.dto.EmailUserInfo\n\ndata class OrderMailDto(\n    val userInfo: EmailUserInfo,\n    val orderInfo: EmailOrderInfo,\n    val eventInfo: EmailEventInfo,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/handler/CreateOrderEventEmailHandler.kt",
    "content": "package band.gosrock.api.email.handler\n\nimport band.gosrock.api.email.service.OrderApproveRequestEmailService\nimport band.gosrock.api.email.service.OrderMailInfoHelper\nimport band.gosrock.domain.common.events.order.CreateOrderEvent\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\nprivate val log = LoggerFactory.getLogger(CreateOrderEventEmailHandler::class.java)\n\n@Component\nclass CreateOrderEventEmailHandler(\n    private val orderApproveRequestEmailService: OrderApproveRequestEmailService,\n    private val orderMailInfoHelper: OrderMailInfoHelper,\n) {\n    @Async\n    @TransactionalEventListener(\n        classes = [CreateOrderEvent::class],\n        phase = TransactionPhase.AFTER_COMMIT,\n    )\n    fun handleDoneOrderFailEvent(createOrderEvent: CreateOrderEvent) {\n        log.info(\"${createOrderEvent.orderUuid}주문 생성 이메일 요청\")\n\n        // 승인 주문만 생성시에 이메일을 보낸다.\n        if (createOrderEvent.orderMethod.isPayment()) {\n            return\n        }\n\n        orderApproveRequestEmailService.execute(\n            orderMailInfoHelper.execute(createOrderEvent.orderUuid),\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/handler/DoneOrderEventEmailHandler.kt",
    "content": "package band.gosrock.api.email.handler\n\nimport band.gosrock.api.email.service.OrderApproveConfirmEmailService\nimport band.gosrock.api.email.service.OrderMailInfoHelper\nimport band.gosrock.api.email.service.OrderPaymentDoneEmailService\nimport band.gosrock.domain.common.events.order.DoneOrderEvent\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\nprivate val log = LoggerFactory.getLogger(DoneOrderEventEmailHandler::class.java)\n\n@Component\nclass DoneOrderEventEmailHandler(\n    private val orderMailInfoHelper: OrderMailInfoHelper,\n    private val orderApproveConfirmEmailService: OrderApproveConfirmEmailService,\n    private val orderPaymentDoneEmailService: OrderPaymentDoneEmailService,\n) {\n    @Async\n    @TransactionalEventListener(\n        classes = [DoneOrderEvent::class],\n        phase = TransactionPhase.AFTER_COMMIT,\n    )\n    fun handleDoneOrderEvent(doneOrderEvent: DoneOrderEvent) {\n        log.info(\"${doneOrderEvent.orderUuid}주문 상태 완료, 이메일 보내기\")\n        val orderMailDto = orderMailInfoHelper.execute(doneOrderEvent.orderUuid)\n\n        // 결제용\n        if (doneOrderEvent.orderMethod.isPayment()) {\n            orderPaymentDoneEmailService.execute(orderMailDto)\n            return\n        }\n        // 승인용\n        orderApproveConfirmEmailService.execute(orderMailDto)\n        log.info(\"${doneOrderEvent.orderUuid}주문 상태 완료, 이메일 보내기 완료\")\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/handler/EntranceIssuedTicketEventEmailHandler.kt",
    "content": "package band.gosrock.api.email.handler\n\nimport band.gosrock.api.email.service.EntranceIssuedTicketEmailService\nimport band.gosrock.api.email.service.IssuedTicketMailInfoHelper\nimport band.gosrock.domain.common.events.issuedTicket.EntranceIssuedTicketEvent\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\nprivate val log = LoggerFactory.getLogger(EntranceIssuedTicketEventEmailHandler::class.java)\n\n@Component\nclass EntranceIssuedTicketEventEmailHandler(\n    private val issuedTicketMailInfoHelper: IssuedTicketMailInfoHelper,\n    private val entranceIssuedTicketEmailService: EntranceIssuedTicketEmailService,\n) {\n    @Async\n    @TransactionalEventListener(\n        classes = [EntranceIssuedTicketEvent::class],\n        phase = TransactionPhase.AFTER_COMMIT,\n    )\n    fun handleEntranceIssuedTicketEvent(entranceIssuedTicketEvent: EntranceIssuedTicketEvent) {\n        log.info(\"${entranceIssuedTicketEvent.issuedTicketNo}번 티켓 입장 처리 이메일 전송\")\n\n        val issuedTicketMailDTO =\n            issuedTicketMailInfoHelper.execute(entranceIssuedTicketEvent.issuedTicketNo)\n        entranceIssuedTicketEmailService.execute(issuedTicketMailDTO)\n        log.info(\"이메일 전송 성공\")\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/handler/RegisterUserEventEmailHandler.kt",
    "content": "package band.gosrock.api.email.handler\n\nimport band.gosrock.api.email.service.SendRegisterEmailService\nimport band.gosrock.domain.common.events.user.UserRegisterEvent\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\nprivate val log = LoggerFactory.getLogger(RegisterUserEventEmailHandler::class.java)\n\n@Component\nclass RegisterUserEventEmailHandler(\n    private val userAdaptor: UserAdaptor,\n    private val sendRegisterEmailService: SendRegisterEmailService,\n) {\n    @Async\n    @TransactionalEventListener(\n        classes = [UserRegisterEvent::class],\n        phase = TransactionPhase.AFTER_COMMIT,\n    )\n    fun handleRegisterUserEvent(userRegisterEvent: UserRegisterEvent) {\n        val userId = userRegisterEvent.userId\n        val user = userAdaptor.queryUser(userId)\n        log.info(\"${userId}유저 등록\")\n        sendRegisterEmailService.execute(user.toEmailUserInfo())\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/handler/WithDrawOrderEventEmailHandler.kt",
    "content": "package band.gosrock.api.email.handler\n\nimport band.gosrock.api.email.service.OrderMailInfoHelper\nimport band.gosrock.api.email.service.OrderWithDrawCancelEmailService\nimport band.gosrock.api.email.service.OrderWithDrawRefundEmailService\nimport band.gosrock.domain.common.events.order.WithDrawOrderEvent\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\n@Component\nclass WithDrawOrderEventEmailHandler(\n    private val orderWithDrawRefundEmailService: OrderWithDrawRefundEmailService,\n    private val orderWithDrawCancelEmailService: OrderWithDrawCancelEmailService,\n    private val orderMailInfoHelper: OrderMailInfoHelper,\n) {\n    @Async\n    @TransactionalEventListener(\n        classes = [WithDrawOrderEvent::class],\n        phase = TransactionPhase.AFTER_COMMIT,\n    )\n    fun handleWithDrawOrderEvent(withDrawOrderEvent: WithDrawOrderEvent) {\n        val orderStatus = withDrawOrderEvent.orderStatus\n        val orderMailDto = orderMailInfoHelper.execute(withDrawOrderEvent.orderUuid)\n\n        // 관리자에 의한 취소\n        if (orderStatus == OrderStatus.CANCELED) {\n            orderWithDrawCancelEmailService.execute(orderMailDto)\n            return\n        }\n        // 구매자에의한 환불 요청\n        orderWithDrawRefundEmailService.execute(orderMailDto)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/service/EntranceIssuedTicketEmailService.kt",
    "content": "package band.gosrock.api.email.service\n\nimport band.gosrock.api.email.dto.IssuedTicketMailDTO\nimport band.gosrock.infrastructure.config.ses.AwsSesUtils\nimport org.springframework.stereotype.Service\nimport org.thymeleaf.context.Context\n\n@Service\nclass EntranceIssuedTicketEmailService(\n    private val awsSesUtils: AwsSesUtils,\n) {\n    fun execute(issuedTicketMailDTO: IssuedTicketMailDTO) {\n        val subject = \"[두둥] 입장 확인 알림드립니다.\"\n        val context = Context()\n        val userInfo = issuedTicketMailDTO.userInfo\n\n        context.setVariable(\"userInfo\", userInfo)\n        context.setVariable(\"issuedTicketInfo\", issuedTicketMailDTO.issuedTicketInfo)\n        context.setVariable(\"eventInfo\", issuedTicketMailDTO.eventInfo)\n\n        awsSesUtils.singleEmailRequest(userInfo, subject, \"entranceIssuedTicket\", context)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/service/HostMasterChangeEmailService.kt",
    "content": "package band.gosrock.api.email.service\n\nimport band.gosrock.domain.domains.host.domain.HostRole\nimport band.gosrock.infrastructure.config.mail.dto.EmailUserInfo\nimport band.gosrock.infrastructure.config.ses.AwsSesUtils\nimport org.slf4j.LoggerFactory\nimport org.springframework.stereotype.Service\nimport org.thymeleaf.context.Context\n\nprivate val log = LoggerFactory.getLogger(HostMasterChangeEmailService::class.java)\n\n@Service\nclass HostMasterChangeEmailService(\n    private val awsSesUtils: AwsSesUtils,\n) {\n    fun execute(userInfo: EmailUserInfo, hostName: String, hostRole: HostRole) {\n        val context = Context()\n        context.setVariable(\"userInfo\", userInfo)\n        context.setVariable(\"hostName\", hostName)\n        context.setVariable(\"role\", hostRole.value)\n        log.info(\"$hostName 에서 마스터 변경 알림, $userInfo\")\n        // todo : 마스터 변경 템플릿 추가\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/service/HostUserDisabledEmailService.kt",
    "content": "package band.gosrock.api.email.service\n\nimport band.gosrock.infrastructure.config.mail.dto.EmailUserInfo\nimport band.gosrock.infrastructure.config.ses.AwsSesUtils\nimport org.slf4j.LoggerFactory\nimport org.springframework.stereotype.Service\nimport org.thymeleaf.context.Context\n\nprivate val log = LoggerFactory.getLogger(HostUserDisabledEmailService::class.java)\n\n@Service\nclass HostUserDisabledEmailService(\n    private val awsSesUtils: AwsSesUtils,\n) {\n    fun execute(userInfo: EmailUserInfo, hostName: String) {\n        val context = Context()\n        context.setVariable(\"userInfo\", userInfo)\n        context.setVariable(\"hostName\", hostName)\n        log.info(\"$hostName 에서 추방당함, $userInfo\")\n        // todo : 호스트에서 추방 템플릿 추가\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/service/HostUserInvitationEmailService.kt",
    "content": "package band.gosrock.api.email.service\n\nimport band.gosrock.domain.domains.host.domain.HostRole\nimport band.gosrock.infrastructure.config.mail.dto.EmailUserInfo\nimport band.gosrock.infrastructure.config.ses.AwsSesUtils\nimport org.springframework.stereotype.Service\nimport org.thymeleaf.context.Context\n\n@Service\nclass HostUserInvitationEmailService(\n    private val awsSesUtils: AwsSesUtils,\n) {\n    fun execute(userInfo: EmailUserInfo, hostName: String, hostRole: HostRole) {\n        val context = Context()\n        context.setVariable(\"userInfo\", userInfo)\n        context.setVariable(\"hostName\", hostName)\n        context.setVariable(\"role\", hostRole.value)\n        awsSesUtils.singleEmailRequest(\n            userInfo,\n            \"두둥$hostName 호스트 초대 알림 드립니다.\",\n            \"hostInvite\",\n            context,\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/service/HostUserRoleChangeEmailService.kt",
    "content": "package band.gosrock.api.email.service\n\nimport band.gosrock.domain.domains.host.domain.HostRole\nimport band.gosrock.infrastructure.config.mail.dto.EmailUserInfo\nimport band.gosrock.infrastructure.config.ses.AwsSesUtils\nimport org.slf4j.LoggerFactory\nimport org.springframework.stereotype.Service\nimport org.thymeleaf.context.Context\n\nprivate val log = LoggerFactory.getLogger(HostUserRoleChangeEmailService::class.java)\n\n@Service\nclass HostUserRoleChangeEmailService(\n    private val awsSesUtils: AwsSesUtils,\n) {\n    fun execute(userInfo: EmailUserInfo, hostName: String, hostRole: HostRole) {\n        val context = Context()\n        context.setVariable(\"userInfo\", userInfo)\n        context.setVariable(\"hostName\", hostName)\n        context.setVariable(\"role\", hostRole.value)\n        log.info(\"$hostName 의 역할 변경 알림. $userInfo\")\n        // todo : 당신의 역할이 변경되었음을 알리는 템플릿 추가\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/service/IssuedTicketMailInfoHelper.kt",
    "content": "package band.gosrock.api.email.service\n\nimport band.gosrock.api.email.dto.IssuedTicketMailDTO\nimport band.gosrock.common.annotation.Helper\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.domain.Host\nimport band.gosrock.domain.domains.issuedTicket.adaptor.IssuedTicketAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.infrastructure.config.mail.dto.EmailEventInfo\nimport org.springframework.transaction.annotation.Transactional\n\n@Helper\n@Transactional(readOnly = true)\nclass IssuedTicketMailInfoHelper(\n    private val userAdaptor: UserAdaptor,\n    private val eventAdaptor: EventAdaptor,\n    private val issuedTicketAdaptor: IssuedTicketAdaptor,\n    private val hostAdaptor: HostAdaptor,\n) {\n    fun execute(issuedTicketNo: String): IssuedTicketMailDTO {\n        val issuedTicket = issuedTicketAdaptor.queryByIssuedTicketNo(issuedTicketNo)\n        val user = userAdaptor.queryUser(issuedTicket.userInfo?.userId)\n        val event = eventAdaptor.findById(issuedTicket.eventId!!)\n        val host = hostAdaptor.findById(event.hostId!!)\n        return IssuedTicketMailDTO(\n            user.toEmailUserInfo(),\n            issuedTicket.toEmailIssuedTicketInfo(),\n            getEventInfo(event, host),\n        )\n    }\n\n    private fun getEventInfo(event: Event, host: Host): EmailEventInfo {\n        return EmailEventInfo(host.profile!!.name!!, event.eventBasic!!.name!!)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/service/OrderApproveConfirmEmailService.kt",
    "content": "package band.gosrock.api.email.service\n\nimport band.gosrock.api.email.dto.OrderMailDto\nimport band.gosrock.infrastructure.config.ses.AwsSesUtils\nimport org.springframework.stereotype.Service\nimport org.thymeleaf.context.Context\n\n@Service\nclass OrderApproveConfirmEmailService(\n    private val awsSesUtils: AwsSesUtils,\n) {\n    fun execute(orderMailDto: OrderMailDto) {\n        val context = Context()\n        val userInfo = orderMailDto.userInfo\n        context.setVariable(\"userInfo\", userInfo)\n        context.setVariable(\"orderInfo\", orderMailDto.orderInfo)\n        context.setVariable(\"eventInfo\", orderMailDto.eventInfo)\n        awsSesUtils.singleEmailRequest(\n            userInfo,\n            \"두둥 주문승인 완료 알림드립니다.\",\n            \"orderApproveConfirm\",\n            context,\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/service/OrderApproveRequestEmailService.kt",
    "content": "package band.gosrock.api.email.service\n\nimport band.gosrock.api.email.dto.OrderMailDto\nimport band.gosrock.infrastructure.config.ses.AwsSesUtils\nimport org.springframework.stereotype.Service\nimport org.thymeleaf.context.Context\n\n@Service\nclass OrderApproveRequestEmailService(\n    private val awsSesUtils: AwsSesUtils,\n) {\n    fun execute(orderMailDto: OrderMailDto) {\n        val context = Context()\n        val userInfo = orderMailDto.userInfo\n        context.setVariable(\"userInfo\", userInfo)\n        context.setVariable(\"orderInfo\", orderMailDto.orderInfo)\n        context.setVariable(\"eventInfo\", orderMailDto.eventInfo)\n        awsSesUtils.singleEmailRequest(\n            userInfo,\n            \"두둥 주문승인 요청 알림드립니다.\",\n            \"orderApproveRequest\",\n            context,\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/service/OrderMailInfoHelper.kt",
    "content": "package band.gosrock.api.email.service\n\nimport band.gosrock.api.email.dto.OrderMailDto\nimport band.gosrock.common.annotation.Helper\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.domain.Host\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.infrastructure.config.mail.dto.EmailEventInfo\nimport org.springframework.transaction.annotation.Transactional\n\n@Helper\n@Transactional(readOnly = true)\nclass OrderMailInfoHelper(\n    private val orderAdaptor: OrderAdaptor,\n    private val userAdaptor: UserAdaptor,\n    private val eventAdaptor: EventAdaptor,\n    private val hostAdaptor: HostAdaptor,\n) {\n    fun execute(orderUuid: String): OrderMailDto {\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        val user = userAdaptor.queryUser(order.userId)\n        val event = eventAdaptor.findById(order.eventId!!)\n        val host = hostAdaptor.findById(event.hostId!!)\n        return OrderMailDto(\n            user.toEmailUserInfo(),\n            order.toEmailOrderInfo(),\n            getEventInfo(event, host),\n        )\n    }\n\n    private fun getEventInfo(event: Event, host: Host): EmailEventInfo {\n        return EmailEventInfo(host.profile!!.name!!, event.eventBasic!!.name!!)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/service/OrderPaymentDoneEmailService.kt",
    "content": "package band.gosrock.api.email.service\n\nimport band.gosrock.api.email.dto.OrderMailDto\nimport band.gosrock.infrastructure.config.ses.AwsSesUtils\nimport org.springframework.stereotype.Service\nimport org.thymeleaf.context.Context\n\n@Service\nclass OrderPaymentDoneEmailService(\n    private val awsSesUtils: AwsSesUtils,\n) {\n    fun execute(orderMailDto: OrderMailDto) {\n        val context = Context()\n        val userInfo = orderMailDto.userInfo\n        context.setVariable(\"userInfo\", userInfo)\n        context.setVariable(\"orderInfo\", orderMailDto.orderInfo)\n        context.setVariable(\"eventInfo\", orderMailDto.eventInfo)\n        awsSesUtils.singleEmailRequest(userInfo, \"두둥 주문 완료 알림드립니다.\", \"orderPaymentDone\", context)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/service/OrderWithDrawCancelEmailService.kt",
    "content": "package band.gosrock.api.email.service\n\nimport band.gosrock.api.email.dto.OrderMailDto\nimport band.gosrock.infrastructure.config.ses.AwsSesUtils\nimport org.springframework.stereotype.Service\nimport org.thymeleaf.context.Context\n\n@Service\nclass OrderWithDrawCancelEmailService(\n    private val awsSesUtils: AwsSesUtils,\n) {\n    fun execute(orderMailDto: OrderMailDto) {\n        val context = Context()\n        val userInfo = orderMailDto.userInfo\n        context.setVariable(\"userInfo\", userInfo)\n        context.setVariable(\"orderInfo\", orderMailDto.orderInfo)\n        context.setVariable(\"eventInfo\", orderMailDto.eventInfo)\n        awsSesUtils.singleEmailRequest(\n            userInfo,\n            \"두둥 주문 철회 알림 드립니다.\",\n            \"orderWithdrawCancel\",\n            context,\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/service/OrderWithDrawRefundEmailService.kt",
    "content": "package band.gosrock.api.email.service\n\nimport band.gosrock.api.email.dto.OrderMailDto\nimport band.gosrock.infrastructure.config.ses.AwsSesUtils\nimport org.springframework.stereotype.Service\nimport org.thymeleaf.context.Context\n\n@Service\nclass OrderWithDrawRefundEmailService(\n    private val awsSesUtils: AwsSesUtils,\n) {\n    fun execute(orderMailDto: OrderMailDto) {\n        val context = Context()\n        val userInfo = orderMailDto.userInfo\n        context.setVariable(\"userInfo\", userInfo)\n        context.setVariable(\"orderInfo\", orderMailDto.orderInfo)\n        context.setVariable(\"eventInfo\", orderMailDto.eventInfo)\n        awsSesUtils.singleEmailRequest(\n            userInfo,\n            \"두둥 주문 철회 알림 드립니다.\",\n            \"orderWithdrawRefund\",\n            context,\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/email/service/SendRegisterEmailService.kt",
    "content": "package band.gosrock.api.email.service\n\nimport band.gosrock.infrastructure.config.mail.dto.EmailUserInfo\nimport band.gosrock.infrastructure.config.ses.AwsSesUtils\nimport org.springframework.stereotype.Service\nimport org.thymeleaf.context.Context\n\n@Service\nclass SendRegisterEmailService(\n    private val awsSesUtils: AwsSesUtils,\n) {\n    fun execute(emailUserInfo: EmailUserInfo) {\n        val context = Context()\n        context.setVariable(\"username\", emailUserInfo.name)\n        awsSesUtils.singleEmailRequest(emailUserInfo, \"두둥에 회원가입하신것을 축하드립니다!\", \"signUp\", context)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/controller/EventController.kt",
    "content": "package band.gosrock.api.event.controller\n\nimport band.gosrock.api.common.slice.SliceResponse\nimport band.gosrock.api.event.model.dto.request.CreateEventRequest\nimport band.gosrock.api.event.model.dto.request.UpdateEventBasicRequest\nimport band.gosrock.api.event.model.dto.request.UpdateEventDetailRequest\nimport band.gosrock.api.event.model.dto.request.UpdateEventStatusRequest\nimport band.gosrock.api.event.model.dto.response.EventChecklistResponse\nimport band.gosrock.api.event.model.dto.response.EventDetailResponse\nimport band.gosrock.api.event.model.dto.response.EventProfileResponse\nimport band.gosrock.api.event.model.dto.response.EventResponse\nimport band.gosrock.api.event.service.CreateEventUseCase\nimport band.gosrock.api.event.service.DeleteEventUseCase\nimport band.gosrock.api.event.service.OpenEventUseCase\nimport band.gosrock.api.event.service.ReadEventChecklistUseCase\nimport band.gosrock.api.event.service.ReadEventDetailUseCase\nimport band.gosrock.api.event.service.ReadUserEventProfilesUseCase\nimport band.gosrock.api.event.service.SearchEventsUseCase\nimport band.gosrock.api.event.service.UpdateEventBasicUseCase\nimport band.gosrock.api.event.service.UpdateEventDetailUseCase\nimport band.gosrock.api.event.service.UpdateEventStatusUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport band.gosrock.common.annotation.DisableSwaggerSecurity\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport jakarta.validation.Valid\nimport org.springdoc.core.annotations.ParameterObject\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.web.PageableDefault\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PatchMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestBody\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RequestParam\nimport org.springframework.web.bind.annotation.RestController\n\n@SecurityRequirement(name = \"access-token\")\n@Tag(name = \"3. [이벤트(공연)]\")\n@RestController\n@RequestMapping(\"/api/v1/events\")\nclass EventController(\n    private val readUserHostEventListUseCase: ReadUserEventProfilesUseCase,\n    private val readEventDetailUseCase: ReadEventDetailUseCase,\n    private val readEventChecklistUseCase: ReadEventChecklistUseCase,\n    private val searchEventsUseCase: SearchEventsUseCase,\n    private val createEventUseCase: CreateEventUseCase,\n    private val updateEventBasicUseCase: UpdateEventBasicUseCase,\n    private val updateEventDetailUseCase: UpdateEventDetailUseCase,\n    private val updateEventStatusUseCase: UpdateEventStatusUseCase,\n    private val openEventUseCase: OpenEventUseCase,\n    private val deleteEventUseCase: DeleteEventUseCase\n) {\n    @Operation(summary = \"자신이 관리 중인 이벤트 리스트를 가져옵니다.\")\n    @GetMapping\n    fun getAllEventByUser(\n        @CurrentUserId userId: Long,\n        @ParameterObject @PageableDefault(size = 10) pageable: Pageable\n    ): SliceResponse<EventProfileResponse> {\n        return readUserHostEventListUseCase.execute(userId, pageable)\n    }\n\n    @Operation(summary = \"이벤트 이름을 키워드로 검색하여 최신순으로 가져옵니다.\")\n    @DisableSwaggerSecurity\n    @GetMapping(\"/search\")\n    fun getAllOpenEventByUser(\n        @RequestParam(required = false) keyword: String?,\n        @ParameterObject @PageableDefault(size = 10) pageable: Pageable\n    ): SliceResponse<EventResponse> {\n        return searchEventsUseCase.execute(keyword, pageable)\n    }\n\n    @Operation(summary = \"공연 기본 정보를 등록하여, 새로운 이벤트(공연)를 생성합니다\")\n    @PostMapping\n    fun createEvent(@CurrentUserId userId: Long, @RequestBody @Valid createEventRequest: CreateEventRequest): EventResponse {\n        return createEventUseCase.execute(userId, createEventRequest)\n    }\n\n    @Operation(summary = \"공연 상세 정보를 가져옵니다.\")\n    @DisableSwaggerSecurity\n    @GetMapping(\"/{eventId}\")\n    fun getEventDetailById(@CurrentUserId userId: Long, @PathVariable eventId: Long): EventDetailResponse {\n        return readEventDetailUseCase.execute(userId, eventId)\n    }\n\n    @Operation(summary = \"공연 체크리스트 가져오기\")\n    @GetMapping(\"/{eventId}/checklist\")\n    fun getEventChecklistById(@CurrentUserId userId: Long, @PathVariable eventId: Long): EventChecklistResponse {\n        return readEventChecklistUseCase.execute(userId, eventId)\n    }\n\n    @Operation(summary = \"공연 기본 정보를 등록하여, 새로운 이벤트(공연)를 생성합니다\")\n    @PatchMapping(\"/{eventId}/basic\")\n    fun updateEventBasic(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @RequestBody @Valid updateEventBasicRequest: UpdateEventBasicRequest\n    ): EventResponse {\n        return updateEventBasicUseCase.execute(userId, eventId, updateEventBasicRequest)\n    }\n\n    @Operation(summary = \"공연 상세 정보를 등록합니다.\")\n    @PatchMapping(\"/{eventId}/details\")\n    fun updateEventDetail(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @RequestBody @Valid updateEventDetailRequest: UpdateEventDetailRequest\n    ): EventResponse {\n        return updateEventDetailUseCase.execute(userId, eventId, updateEventDetailRequest)\n    }\n\n    @Operation(summary = \"공연을 오픈 상태로 변경합니다. 모든 체크리스트를 달성해야 합니다.\")\n    @PatchMapping(\"/{eventId}/open\")\n    fun updateEventOpen(@CurrentUserId userId: Long, @PathVariable eventId: Long): EventResponse {\n        return openEventUseCase.execute(userId, eventId)\n    }\n\n    @Operation(summary = \"공연 상태를 변경합니다. (OPEN 제외)\")\n    @PatchMapping(\"/{eventId}/status\")\n    fun updateEventStatus(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @RequestBody @Valid updateEventDetailRequest: UpdateEventStatusRequest\n    ): EventResponse {\n        return updateEventStatusUseCase.execute(userId, eventId, updateEventDetailRequest)\n    }\n\n    @Operation(summary = \"공연을 삭제합니다. 조건에 맞지 않을 경우 삭제할 수 없습니다.\")\n    @PatchMapping(\"/{eventId}/delete\")\n    fun deleteEvent(@CurrentUserId userId: Long, @PathVariable eventId: Long): EventResponse {\n        return deleteEventUseCase.execute(userId, eventId)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/model/dto/request/CreateEventRequest.kt",
    "content": "package band.gosrock.api.event.model.dto.request\n\nimport band.gosrock.common.annotation.DateFormat\nimport io.swagger.v3.oas.annotations.media.Schema\nimport java.time.LocalDateTime\nimport jakarta.validation.constraints.Future\nimport jakarta.validation.constraints.NotBlank\nimport jakarta.validation.constraints.NotNull\nimport jakarta.validation.constraints.Positive\nimport org.hibernate.validator.constraints.Length\n\ndata class CreateEventRequest(\n    @field:Schema(defaultValue = \"1\", description = \"호스트 고유 아이디\")\n    @field:Positive\n    val hostId: Long? = null,\n\n    @field:Schema(defaultValue = \"고스락 제 22회 정기공연\", description = \"공연 이름\")\n    @field:NotBlank(message = \"공연 이름을 입력하세요.\")\n    @field:Length(max = 25)\n    val name: String? = null,\n\n    @field:Schema(\n        type = \"string\",\n        pattern = \"yyyy.MM.dd HH:mm\",\n        defaultValue = \"2023.03.20 12:00\",\n        description = \"공연 시작 시각\"\n    )\n    @field:NotNull(message = \"공연 시작 시각을 입력하세요.\")\n    @field:Future(message = \"공연 시작 시각은 현재보다 이후여야 합니다.\")\n    @field:DateFormat\n    val startAt: LocalDateTime? = null,\n\n    @field:Schema(defaultValue = \"90\", description = \"공연 진행시간\")\n    @field:Positive(message = \"공연 진행 예상 소요시간(분)을 입력하세요.\")\n    val runTime: Long? = null\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/model/dto/request/UpdateEventBasicRequest.kt",
    "content": "package band.gosrock.api.event.model.dto.request\n\nimport band.gosrock.common.annotation.DateFormat\nimport io.swagger.v3.oas.annotations.media.Schema\nimport java.time.LocalDateTime\nimport jakarta.validation.constraints.Future\nimport jakarta.validation.constraints.NotBlank\nimport jakarta.validation.constraints.NotNull\nimport jakarta.validation.constraints.Positive\nimport org.hibernate.validator.constraints.Length\n\ndata class UpdateEventBasicRequest(\n    @field:Schema(defaultValue = \"고스락 제 22회 정기공연\", description = \"공연 이름\")\n    @field:NotBlank(message = \"공연 이름을 입력하세요\")\n    @field:Length(max = 25)\n    val name: String? = null,\n\n    @field:Schema(\n        type = \"string\",\n        pattern = \"yyyy.MM.dd HH:mm\",\n        defaultValue = \"2023.03.20 12:00\",\n        description = \"공연 시작 시각\"\n    )\n    @field:NotNull(message = \"공연 시작 시각을 입력하세요\")\n    @field:Future(message = \"공연 시작 시각은 현재보다 이후여야 합니다.\")\n    @field:DateFormat\n    val startAt: LocalDateTime? = null,\n\n    @field:Schema(defaultValue = \"90\", description = \"공연 진행시간\")\n    @field:Positive(message = \"공연 진행 예상 소요시간(분)을 입력하세요\")\n    val runTime: Long? = null,\n\n    // 공연 장소\n    @field:Schema(defaultValue = \"롤링홀\", description = \"공연장 이름\")\n    @field:NotBlank(message = \"공연장 이름을 입력하세요\")\n    val placeName: String? = null,\n\n    // 공연 상세 주소\n    @field:Schema(defaultValue = \"서울 마포구 어울마당로 35\", description = \"공연장 주소\")\n    @field:NotBlank(message = \"공연장 상세주소를 입력하세요\")\n    val placeAddress: String? = null,\n\n    // (지도 정보) 경도 - x\n    @field:Schema(defaultValue = \"126.920036\", description = \"공연장 위치 경도\")\n    @field:Positive(message = \"공연장 경도 정보를 입력하세요\")\n    val longitude: Double? = null,\n\n    // (지도 정보) 위도 - y\n    @field:Schema(defaultValue = \"37.548369\", description = \"공연장 위치 위도\")\n    @field:Positive(message = \"공연장 위도 정보를 입력하세요\")\n    val latitude: Double? = null\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/model/dto/request/UpdateEventDetailRequest.kt",
    "content": "package band.gosrock.api.event.model.dto.request\n\nimport io.swagger.v3.oas.annotations.media.Schema\nimport jakarta.validation.constraints.NotBlank\nimport jakarta.validation.constraints.NotEmpty\n\ndata class UpdateEventDetailRequest(\n    // 포스터 이미지 url\n    @field:Schema(defaultValue = \"test/event/5/aa.jpeg\", description = \"포스터 이미지 key\")\n    @field:NotEmpty(message = \"포스터 이미지 키 값을 입력해주세요\")\n    val posterImageKey: String? = null,\n\n    // (마크다운) 공연 상세 내용\n    @field:Schema(defaultValue = \"고스락 공연에 여러분을 초대합니다 웰컴웰컴\", description = \"공연 상세 내용\")\n    @field:NotBlank(message = \"공연장 상세 내용을 입력하세요\")\n    val content: String? = null\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/model/dto/request/UpdateEventStatusRequest.kt",
    "content": "package band.gosrock.api.event.model.dto.request\n\nimport band.gosrock.common.annotation.Enum\nimport band.gosrock.domain.domains.event.domain.EventStatus\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class UpdateEventStatusRequest(\n    @field:Schema(defaultValue = \"OPEN\", description = \"오픈 상태\")\n    @field:Enum(message = \"올바른 값을 입력해주세요.\")\n    val status: EventStatus? = null\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/model/dto/response/EventChecklistResponse.kt",
    "content": "package band.gosrock.api.event.model.dto.response\n\nimport band.gosrock.domain.domains.event.domain.Event\n\n/** 공연 체크 리스트 응답 DTO */\ndata class EventChecklistResponse(\n    val hostId: Long?,\n    val eventId: Long?,\n    val name: String?,\n    // 공연 기본 정보 (공연장 위치로 구분) 작성 여부\n    val hasBasic: Boolean?,\n    // 공연 상세 정보 작성 여부\n    val hasDetail: Boolean?,\n    // 티켓 상품 설정했는지 여부\n    val hasTicketItem: Boolean?\n) {\n    companion object {\n        @JvmStatic\n        fun of(event: Event, hasTicket: Boolean): EventChecklistResponse {\n            return EventChecklistResponse(\n                hostId = event.hostId,\n                eventId = event.id,\n                name = event.eventBasic?.name,\n                hasBasic = event.hasEventBasic() && event.hasEventPlace(),\n                hasDetail = event.hasEventDetail(),\n                hasTicketItem = hasTicket\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/model/dto/response/EventDetailResponse.kt",
    "content": "package band.gosrock.api.event.model.dto.response\n\nimport band.gosrock.domain.common.vo.EventBasicVo\nimport band.gosrock.domain.common.vo.EventDetailVo\nimport band.gosrock.domain.common.vo.EventPlaceVo\nimport band.gosrock.domain.common.vo.HostInfoVo\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.event.domain.EventStatus\nimport band.gosrock.domain.domains.host.domain.Host\nimport com.fasterxml.jackson.annotation.JsonUnwrapped\n\n/** 이벤트 디테일 응답 DTO */\ndata class EventDetailResponse(\n    val status: EventStatus?,\n    val host: HostInfoVo?,\n    @JsonUnwrapped val eventBasicVo: EventBasicVo?,\n    val place: EventPlaceVo?,\n    @JsonUnwrapped val eventDetailVo: EventDetailVo?\n) {\n    companion object {\n        @JvmStatic\n        fun of(host: Host, event: Event): EventDetailResponse {\n            return EventDetailResponse(\n                eventBasicVo = event.toEventBasicVo(),\n                eventDetailVo = event.toEventDetailVo(),\n                place = event.toEventPlaceVo(),\n                host = host.toHostInfoVo(),\n                status = event.status\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/model/dto/response/EventProfileResponse.kt",
    "content": "package band.gosrock.api.event.model.dto.response\n\nimport band.gosrock.domain.common.vo.EventProfileVo\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.host.domain.Host\nimport com.fasterxml.jackson.annotation.JsonUnwrapped\n\n/** 이벤트 프로필만 표시하는 응답 DTO */\ndata class EventProfileResponse(\n    val hostId: Long?,\n    val hostName: String?,\n    @JsonUnwrapped val eventProfileVo: EventProfileVo?\n) {\n    companion object {\n        @JvmStatic\n        fun of(host: Host, event: Event): EventProfileResponse {\n            return EventProfileResponse(\n                hostId = host.id,\n                hostName = host.profile!!.name,\n                eventProfileVo = event.toEventProfileVo()\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/model/dto/response/EventResponse.kt",
    "content": "package band.gosrock.api.event.model.dto.response\n\nimport band.gosrock.domain.common.vo.EventBasicVo\nimport band.gosrock.domain.common.vo.EventDetailVo\nimport band.gosrock.domain.common.vo.EventPlaceVo\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.event.domain.EventStatus\nimport com.fasterxml.jackson.annotation.JsonUnwrapped\n\ndata class EventResponse(\n    val eventId: Long?,\n    val hostId: Long?,\n    val status: EventStatus?,\n    @JsonUnwrapped val eventBasic: EventBasicVo?,\n    @JsonUnwrapped val eventDetail: EventDetailVo?,\n    val place: EventPlaceVo?\n) {\n    companion object {\n        @JvmStatic\n        fun of(event: Event): EventResponse {\n            return EventResponse(\n                eventId = event.id,\n                hostId = event.hostId,\n                eventBasic = EventBasicVo.from(event),\n                eventDetail = EventDetailVo.from(event),\n                place = EventPlaceVo.from(event),\n                status = event.status\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/model/mapper/EventMapper.kt",
    "content": "package band.gosrock.api.event.model.mapper\n\nimport band.gosrock.api.event.model.dto.request.CreateEventRequest\nimport band.gosrock.api.event.model.dto.request.UpdateEventBasicRequest\nimport band.gosrock.api.event.model.dto.request.UpdateEventDetailRequest\nimport band.gosrock.api.event.model.dto.response.EventChecklistResponse\nimport band.gosrock.api.event.model.dto.response.EventDetailResponse\nimport band.gosrock.api.event.model.dto.response.EventProfileResponse\nimport band.gosrock.api.event.model.dto.response.EventResponse\nimport band.gosrock.common.annotation.Mapper\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.event.domain.EventBasic\nimport band.gosrock.domain.domains.event.domain.EventDetail\nimport band.gosrock.domain.domains.event.domain.EventPlace\nimport band.gosrock.domain.domains.event.domain.EventStatus\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.domain.Host\nimport band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.domain.Slice\n\n@Mapper\nclass EventMapper(\n    private val hostAdaptor: HostAdaptor,\n    private val eventAdaptor: EventAdaptor,\n    private val ticketItemAdaptor: TicketItemAdaptor\n) {\n\n    fun toEntity(createEventRequest: CreateEventRequest): Event {\n        return Event(\n            hostId = createEventRequest.hostId,\n            name = createEventRequest.name,\n            startAt = createEventRequest.startAt,\n            runTime = createEventRequest.runTime,\n        )\n    }\n\n    fun toEventBasic(updateEventBasicRequest: UpdateEventBasicRequest): EventBasic {\n        return EventBasic(\n            name = updateEventBasicRequest.name,\n            runTime = updateEventBasicRequest.runTime,\n            startAt = updateEventBasicRequest.startAt,\n        )\n    }\n\n    fun toEventDetail(updateEventDetailRequest: UpdateEventDetailRequest): EventDetail {\n        return EventDetail(\n            posterImageKey = updateEventDetailRequest.posterImageKey,\n            content = updateEventDetailRequest.content,\n        )\n    }\n\n    fun toEventPlace(updateEventBasicRequest: UpdateEventBasicRequest): EventPlace {\n        return EventPlace(\n            placeName = updateEventBasicRequest.placeName,\n            placeAddress = updateEventBasicRequest.placeAddress,\n            latitude = updateEventBasicRequest.latitude,\n            longitude = updateEventBasicRequest.longitude,\n        )\n    }\n\n    fun toEventDetailResponse(host: Host, event: Event): EventDetailResponse {\n        return EventDetailResponse.of(host, event)\n    }\n\n    fun toEventChecklistResponse(event: Event): EventChecklistResponse {\n        val hasTicketItem = ticketItemAdaptor.existsByEventId(event.id!!)\n        return EventChecklistResponse.of(event, hasTicketItem)\n    }\n\n    fun toEventProfileResponsePage(userId: Long, pageable: Pageable): Page<EventProfileResponse> {\n        val hostList = hostAdaptor.findAllByHostUsers_UserId(userId)\n        val hostIds = hostList.map { it.id!! }\n        val eventList = eventAdaptor.findAllByHostIdIn(hostIds, pageable)\n        return eventList.map { event -> toEventProfileResponse(hostList, event) }\n    }\n\n    fun toEventProfileResponseSlice(userId: Long, pageable: Pageable): Slice<EventProfileResponse> {\n        val hosts = hostAdaptor.querySliceHostsByActiveUserId(userId)\n        val hostIds = hosts.map { it.id!! }\n        val events = eventAdaptor.querySliceEventsByHostIdIn(hostIds, pageable)\n        return events.map { event -> toEventProfileResponse(hosts, event) }\n    }\n\n    fun toEventResponseSliceByStatus(status: EventStatus, pageable: Pageable): Slice<EventResponse> {\n        val events = eventAdaptor.querySliceEventsByStatus(status, pageable)\n        return events.map { EventResponse.of(it) }\n    }\n\n    fun toEventResponseSliceByKeyword(keyword: String?, pageable: Pageable): Slice<EventResponse> {\n        val events = eventAdaptor.querySliceEventsByKeyword(keyword, pageable)\n        return events.map { EventResponse.of(it) }\n    }\n\n    private fun toEventProfileResponse(hostList: List<Host>, event: Event): EventProfileResponse? {\n        for (host in hostList) {\n            if (host.id == event.hostId) return EventProfileResponse.of(host, event)\n        }\n        return null\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/service/CreateEventUseCase.kt",
    "content": "package band.gosrock.api.event.service\n\nimport band.gosrock.api.event.model.dto.request.CreateEventRequest\nimport band.gosrock.api.event.model.dto.response.EventResponse\nimport band.gosrock.api.event.model.mapper.EventMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.service.EventService\nimport band.gosrock.domain.domains.host.service.HostService\nimport org.slf4j.LoggerFactory\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass CreateEventUseCase(\n    private val hostService: HostService,\n    private val eventService: EventService,\n    private val eventMapper: EventMapper\n) {\n    private val log = LoggerFactory.getLogger(CreateEventUseCase::class.java)\n\n    @Transactional\n    fun execute(userId: Long, createEventRequest: CreateEventRequest): EventResponse {\n        log.info(\"[CreateEventUseCase][execute] 이벤트 생성 userId={} hostId={}\", userId, createEventRequest.hostId)\n        // 슈퍼 호스트 이상만 공연 생성 가능\n        hostService.validateManagerHostUser(createEventRequest.hostId!!, userId)\n        val event = eventMapper.toEntity(createEventRequest)\n        return EventResponse.of(eventService.createEvent(event))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/service/DeleteEventUseCase.kt",
    "content": "package band.gosrock.api.event.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.MANAGER\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.event.model.dto.response.EventResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.service.EventService\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass DeleteEventUseCase(\n    private val eventService: EventService,\n    private val eventAdaptor: EventAdaptor\n) {\n    @Transactional\n    @HostRolesAllowed(role = MANAGER, findHostFrom = EVENT_ID)\n    fun execute(userId: Long, eventId: Long): EventResponse {\n        val event = eventAdaptor.findById(eventId)\n        return EventResponse.of(eventService.deleteEventSoft(event))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/service/OpenEventUseCase.kt",
    "content": "package band.gosrock.api.event.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.MANAGER\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.event.model.dto.response.EventResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.service.EventService\nimport org.slf4j.LoggerFactory\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass OpenEventUseCase(\n    private val eventService: EventService,\n    private val eventAdaptor: EventAdaptor\n) {\n    private val log = LoggerFactory.getLogger(OpenEventUseCase::class.java)\n\n    @Transactional\n    @HostRolesAllowed(role = MANAGER, findHostFrom = EVENT_ID)\n    fun execute(userId: Long, eventId: Long): EventResponse {\n        log.info(\"[OpenEventUseCase][execute] 이벤트 오픈 userId={} eventId={}\", userId, eventId)\n        val event = eventAdaptor.findById(eventId)\n        return EventResponse.of(eventService.openEvent(event))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/service/ReadEventChecklistUseCase.kt",
    "content": "package band.gosrock.api.event.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.GUEST\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.event.model.dto.response.EventChecklistResponse\nimport band.gosrock.api.event.model.mapper.EventMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass ReadEventChecklistUseCase(\n    private val eventAdaptor: EventAdaptor,\n    private val eventMapper: EventMapper\n) {\n    @HostRolesAllowed(role = GUEST, findHostFrom = EVENT_ID)\n    fun execute(userId: Long, eventId: Long): EventChecklistResponse {\n        val event = eventAdaptor.findById(eventId)\n        return eventMapper.toEventChecklistResponse(event)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/service/ReadEventDetailUseCase.kt",
    "content": "package band.gosrock.api.event.service\n\nimport band.gosrock.api.event.model.dto.response.EventDetailResponse\nimport band.gosrock.api.event.model.mapper.EventMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.exception.EventNotOpenException\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass ReadEventDetailUseCase(\n    private val eventAdaptor: EventAdaptor,\n    private val eventMapper: EventMapper,\n    private val hostAdaptor: HostAdaptor\n) {\n    fun execute(userId: Long, eventId: Long): EventDetailResponse {\n        val event = eventAdaptor.findById(eventId)\n        val host = hostAdaptor.findById(event.hostId!!)\n        // 호스트 유저가 아닐 경우 준비 상태일 때 조회할 수 없음\n        if (event.isPreparing() && !host.isActiveHostUserId(userId)) {\n            throw EventNotOpenException.EXCEPTION\n        }\n        return eventMapper.toEventDetailResponse(host, event)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/service/ReadUserEventProfilesUseCase.kt",
    "content": "package band.gosrock.api.event.service\n\nimport band.gosrock.api.common.slice.SliceResponse\nimport band.gosrock.api.event.model.dto.response.EventProfileResponse\nimport band.gosrock.api.event.model.mapper.EventMapper\nimport band.gosrock.common.annotation.UseCase\nimport org.springframework.data.domain.Pageable\nimport org.springframework.transaction.annotation.Transactional\n\n/** 해당 호스트 유저가 관리중인 이벤트 리스트를 불러오는 유즈케이스 */\n@UseCase\n@Transactional(readOnly = true)\nclass ReadUserEventProfilesUseCase(\n    private val eventMapper: EventMapper\n) {\n    fun execute(userId: Long, pageable: Pageable): SliceResponse<EventProfileResponse> {\n        return SliceResponse.of(eventMapper.toEventProfileResponseSlice(userId, pageable))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/service/SearchEventsUseCase.kt",
    "content": "package band.gosrock.api.event.service\n\nimport band.gosrock.api.common.slice.SliceResponse\nimport band.gosrock.api.event.model.dto.response.EventResponse\nimport band.gosrock.api.event.model.mapper.EventMapper\nimport band.gosrock.common.annotation.UseCase\nimport org.springframework.data.domain.Pageable\nimport org.springframework.transaction.annotation.Transactional\n\n/** 해당 호스트 유저가 관리중인 이벤트 리스트를 불러오는 유즈케이스 */\n@UseCase\n@Transactional(readOnly = true)\nclass SearchEventsUseCase(\n    private val eventMapper: EventMapper\n) {\n    fun execute(keyword: String?, pageable: Pageable): SliceResponse<EventResponse> {\n        return SliceResponse.of(eventMapper.toEventResponseSliceByKeyword(keyword, pageable))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/service/UpdateEventBasicUseCase.kt",
    "content": "package band.gosrock.api.event.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.MANAGER\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.event.model.dto.request.UpdateEventBasicRequest\nimport band.gosrock.api.event.model.dto.response.EventResponse\nimport band.gosrock.api.event.model.mapper.EventMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.service.EventService\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass UpdateEventBasicUseCase(\n    private val eventService: EventService,\n    private val eventAdaptor: EventAdaptor,\n    private val eventMapper: EventMapper\n) {\n    @Transactional\n    @HostRolesAllowed(role = MANAGER, findHostFrom = EVENT_ID)\n    fun execute(userId: Long, eventId: Long, updateEventBasicRequest: UpdateEventBasicRequest): EventResponse {\n        val event = eventAdaptor.findById(eventId)\n        val eventBasic = eventMapper.toEventBasic(updateEventBasicRequest)\n        val eventPlace = eventMapper.toEventPlace(updateEventBasicRequest)\n        return EventResponse.of(eventService.updateEventBasic(event, eventBasic, eventPlace))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/service/UpdateEventDetailUseCase.kt",
    "content": "package band.gosrock.api.event.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.MANAGER\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.event.model.dto.request.UpdateEventDetailRequest\nimport band.gosrock.api.event.model.dto.response.EventResponse\nimport band.gosrock.api.event.model.mapper.EventMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.service.EventService\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass UpdateEventDetailUseCase(\n    private val eventService: EventService,\n    private val eventAdaptor: EventAdaptor,\n    private val eventMapper: EventMapper\n) {\n    @Transactional\n    @HostRolesAllowed(role = MANAGER, findHostFrom = EVENT_ID)\n    fun execute(userId: Long, eventId: Long, updateEventDetailRequest: UpdateEventDetailRequest): EventResponse {\n        val event = eventAdaptor.findById(eventId)\n        return EventResponse.of(\n            eventService.updateEventDetail(event, eventMapper.toEventDetail(updateEventDetailRequest))\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/event/service/UpdateEventStatusUseCase.kt",
    "content": "package band.gosrock.api.event.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.MANAGER\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.event.model.dto.request.UpdateEventStatusRequest\nimport band.gosrock.api.event.model.dto.response.EventResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.service.EventService\nimport org.slf4j.LoggerFactory\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass UpdateEventStatusUseCase(\n    private val eventService: EventService,\n    private val eventAdaptor: EventAdaptor\n) {\n    private val log = LoggerFactory.getLogger(UpdateEventStatusUseCase::class.java)\n\n    @Transactional\n    @HostRolesAllowed(role = MANAGER, findHostFrom = EVENT_ID)\n    fun execute(userId: Long, eventId: Long, updateEventStatusRequest: UpdateEventStatusRequest): EventResponse {\n        log.info(\"[UpdateEventStatusUseCase][execute] 이벤트 상태 변경 userId={} eventId={} status={}\", userId, eventId, updateEventStatusRequest.status)\n        val event = eventAdaptor.findById(eventId)\n        val status = updateEventStatusRequest.status!!\n        return EventResponse.of(eventService.updateEventStatus(event, status))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/example/controller/ExampleController.kt",
    "content": "package band.gosrock.api.example.controller\n\nimport band.gosrock.api.example.docs.ExampleException2Docs\nimport band.gosrock.api.example.docs.ExampleExceptionDocs\nimport band.gosrock.api.example.dto.ExampleResponse\nimport band.gosrock.api.example.service.ExampleApiService\nimport band.gosrock.common.annotation.ApiErrorCodeExample\nimport band.gosrock.common.annotation.ApiErrorExceptionsExample\nimport band.gosrock.common.annotation.DevelopOnlyApi\nimport band.gosrock.common.annotation.DisableSwaggerSecurity\nimport band.gosrock.common.exception.GlobalErrorCode\nimport band.gosrock.domain.domains.cart.exception.CartErrorCode\nimport band.gosrock.domain.domains.coupon.exception.CouponErrorCode\nimport band.gosrock.domain.domains.event.exception.EventErrorCode\nimport band.gosrock.domain.domains.host.exception.HostErrorCode\nimport band.gosrock.domain.domains.issuedTicket.exception.IssuedTicketErrorCode\nimport band.gosrock.domain.domains.order.exception.OrderErrorCode\nimport band.gosrock.domain.domains.ticket_item.exception.TicketItemErrorCode\nimport band.gosrock.domain.domains.user.exception.UserErrorCode\nimport band.gosrock.infrastructure.outer.api.oauth.exception.KakaoKauthErrorCode\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.PaymentsCancelErrorCode\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.PaymentsConfirmErrorCode\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.PaymentsCreateErrorCode\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.TransactionGetErrorCode\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RestController\n\n@RestController\n@RequestMapping(\"/api/v1/examples\")\n@SecurityRequirement(name = \"access-token\")\n@Tag(name = \"xx. [예시] 에러코드 문서화\")\nclass ExampleController(\n    private val exampleApiService: ExampleApiService,\n) {\n    @GetMapping\n    @DevelopOnlyApi\n    @ApiErrorExceptionsExample(ExampleExceptionDocs::class)\n    fun get(): ExampleResponse = exampleApiService.getExample()\n\n    @GetMapping(\"/health\")\n    @DisableSwaggerSecurity\n    fun health() {}\n\n    @PostMapping\n    @ApiErrorExceptionsExample(ExampleException2Docs::class)\n    fun create(): ExampleResponse = exampleApiService.createExample()\n\n    @GetMapping(\"/global\")\n    @DevelopOnlyApi\n    @Operation(summary = \"글로벌 ( 인증 , aop, 서버 내부 오류등)  관련 에러 코드 나열\")\n    @ApiErrorCodeExample(GlobalErrorCode::class)\n    fun getGlobalErrorCode() {}\n\n    @GetMapping(\"/user\")\n    @DevelopOnlyApi\n    @Operation(summary = \"유저 도메인 관련 에러 코드 나열\")\n    @ApiErrorCodeExample(UserErrorCode::class)\n    fun getUserErrorCode() {}\n\n    @GetMapping(\"/order\")\n    @DevelopOnlyApi\n    @Operation(summary = \"주문 도메인 관련 에러 코드 나열\")\n    @ApiErrorCodeExample(OrderErrorCode::class)\n    fun getOrderErrorCode() {}\n\n    @GetMapping(\"/cart\")\n    @DevelopOnlyApi\n    @Operation(summary = \"주문 도메인 관련 에러 코드 나열\")\n    @ApiErrorCodeExample(CartErrorCode::class)\n    fun getCartErrorCode() {}\n\n    @GetMapping(\"/issuedTicket\")\n    @DevelopOnlyApi\n    @Operation(summary = \"티켓 발급 관련 에러 코드 나열\")\n    @ApiErrorCodeExample(IssuedTicketErrorCode::class)\n    fun getIssuedTicketErrorCode() {}\n\n    @GetMapping(\"/kakao\")\n    @DevelopOnlyApi\n    @Operation(summary = \"카카오 에러 코드 나열\")\n    @ApiErrorCodeExample(KakaoKauthErrorCode::class)\n    fun getKakaoKauthErrorCode() {}\n\n    @GetMapping(\"/toss/create\")\n    @DevelopOnlyApi\n    @Operation(summary = \"토스 주문 생성 관련 에러 코드 나열\")\n    @ApiErrorCodeExample(PaymentsCreateErrorCode::class)\n    fun getPaymentsCreateErrorCode() {}\n\n    @GetMapping(\"/toss/confirm\")\n    @DevelopOnlyApi\n    @Operation(summary = \"토스 주문 승인 관련 에러 코드 나열\")\n    @ApiErrorCodeExample(PaymentsConfirmErrorCode::class)\n    fun getPaymentsConfirmErrorCode() {}\n\n    @GetMapping(\"/toss/cancel\")\n    @DevelopOnlyApi\n    @Operation(summary = \"토스 주문 취소 관련 에러 코드 나열\")\n    @ApiErrorCodeExample(PaymentsCancelErrorCode::class)\n    fun getPaymentsCancelErrorCode() {}\n\n    @GetMapping(\"/toss/transaction\")\n    @DevelopOnlyApi\n    @Operation(summary = \"토스 거래 조회 관련 에러 코드 나열\")\n    @ApiErrorCodeExample(TransactionGetErrorCode::class)\n    fun getTransactionGetErrorCode() {}\n\n    @GetMapping(\"/coupon\")\n    @DevelopOnlyApi\n    @Operation(summary = \"쿠폰 도메인 관련 에러 코드 나열\")\n    @ApiErrorCodeExample(CouponErrorCode::class)\n    fun getCouponErrorCode() {}\n\n    @GetMapping(\"/ticketItem\")\n    @DevelopOnlyApi\n    @Operation(summary = \"티켓 상품 관련 에러 코드 나열\")\n    @ApiErrorCodeExample(TicketItemErrorCode::class)\n    fun getTicketItemErrorCode() {}\n\n    @GetMapping(\"/host\")\n    @DevelopOnlyApi\n    @Operation(summary = \"호스트 도메인 관련 에러 코드 나열\")\n    @ApiErrorCodeExample(HostErrorCode::class)\n    fun getHostErrorCode() {}\n\n    @GetMapping(\"/event\")\n    @DevelopOnlyApi\n    @Operation(summary = \"이벤트 도메인 관련 에러 코드 나열\")\n    @ApiErrorCodeExample(EventErrorCode::class)\n    fun getEventErrorCode() {}\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/example/docs/ExampleException2Docs.kt",
    "content": "package band.gosrock.api.example.docs\n\nimport band.gosrock.common.annotation.ExceptionDoc\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.exception.DuDoongCodeException\nimport band.gosrock.common.exception.InvalidTokenException\nimport band.gosrock.common.interfaces.SwaggerExampleExceptions\nimport band.gosrock.domain.domains.order.exception.InvalidOrderException\nimport band.gosrock.domain.domains.order.exception.OrderNotFoundException\n\n@ExceptionDoc\nclass ExampleException2Docs : SwaggerExampleExceptions {\n\n    @ExplainError(\"어쩌구 저쩌구\")\n    val 유저없을때: DuDoongCodeException = InvalidTokenException.EXCEPTION\n\n    @ExplainError(\"오더 낫파운드\")\n    val 한글도된다: DuDoongCodeException = OrderNotFoundException.EXCEPTION\n\n    @ExplainError(\"인밸리드 오더\")\n    val 오류가났을때: DuDoongCodeException = InvalidOrderException.EXCEPTION\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/example/docs/ExampleExceptionDocs.kt",
    "content": "package band.gosrock.api.example.docs\n\nimport band.gosrock.common.annotation.ExceptionDoc\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.exception.DuDoongCodeException\nimport band.gosrock.common.interfaces.SwaggerExampleExceptions\nimport band.gosrock.domain.domains.order.exception.InvalidOrderException\nimport band.gosrock.domain.domains.order.exception.OrderNotFoundException\nimport band.gosrock.domain.domains.user.exception.UserNotFoundException\n\n@ExceptionDoc\nclass ExampleExceptionDocs : SwaggerExampleExceptions {\n\n    @ExplainError(\"유저검색시에 안나올 때 나오는 에러입니다.\")\n    val 유저없을때: DuDoongCodeException = UserNotFoundException.EXCEPTION\n\n    @ExplainError(\"오더 낫파운드\")\n    val 한글도된다: DuDoongCodeException = OrderNotFoundException.EXCEPTION\n\n    @ExplainError(\"인밸리드 오더\")\n    val 오류가났을때: DuDoongCodeException = InvalidOrderException.EXCEPTION\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/example/dto/ExampleResponse.kt",
    "content": "package band.gosrock.api.example.dto\n\nimport band.gosrock.domain.domains.example.domain.ExampleEntity\n\ndata class ExampleResponse(\n    val id: Long,\n    val content: String,\n) {\n    companion object {\n        @JvmStatic\n        fun from(exampleEntity: ExampleEntity): ExampleResponse =\n            ExampleResponse(\n                id = exampleEntity.id!!,\n                content = exampleEntity.content!!,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/example/service/ExampleApiService.kt",
    "content": "package band.gosrock.api.example.service\n\nimport band.gosrock.api.example.dto.ExampleResponse\nimport band.gosrock.domain.domains.example.service.ExampleDomainService\nimport org.springframework.stereotype.Service\n\n@Service\nclass ExampleApiService(\n    private val exampleDomainService: ExampleDomainService,\n) {\n    fun getExample(): ExampleResponse {\n        val query = exampleDomainService.query(1L)\n        return ExampleResponse.from(query)\n    }\n\n    fun createExample(): ExampleResponse {\n        val asdf = exampleDomainService.save(\"asdf\")\n        return ExampleResponse.from(asdf)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/controller/HostController.kt",
    "content": "package band.gosrock.api.host.controller\n\nimport band.gosrock.api.common.page.PageResponse\nimport band.gosrock.api.common.slice.SliceResponse\nimport band.gosrock.api.host.model.dto.request.CreateHostRequest\nimport band.gosrock.api.host.model.dto.request.InviteHostRequest\nimport band.gosrock.api.host.model.dto.request.UpdateHostRequest\nimport band.gosrock.api.host.model.dto.request.UpdateHostSlackRequest\nimport band.gosrock.api.host.model.dto.request.TransferMasterRequest\nimport band.gosrock.api.host.model.dto.request.UpdateHostUserRoleRequest\nimport band.gosrock.api.host.model.dto.response.HostDetailResponse\nimport band.gosrock.api.host.model.dto.response.HostEventProfileResponse\nimport band.gosrock.api.host.model.dto.response.HostProfileResponse\nimport band.gosrock.api.host.model.dto.response.HostResponse\nimport band.gosrock.api.host.service.CreateHostUseCase\nimport band.gosrock.api.host.service.InviteHostUseCase\nimport band.gosrock.api.host.service.JoinHostUseCase\nimport band.gosrock.api.host.service.ReadHostEventsUseCase\nimport band.gosrock.api.host.service.ReadHostProfilesUseCase\nimport band.gosrock.api.host.service.ReadHostUseCase\nimport band.gosrock.api.host.service.TransferMasterUseCase\nimport band.gosrock.api.host.service.ReadInviteUsersUseCase\nimport band.gosrock.api.host.service.RejectHostUseCase\nimport band.gosrock.api.host.service.UpdateHostProfileUseCase\nimport band.gosrock.api.host.service.UpdateHostSlackUrlUseCase\nimport band.gosrock.api.host.service.UpdateHostUserRoleUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport band.gosrock.domain.common.vo.UserProfileVo\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport jakarta.validation.Valid\nimport jakarta.validation.constraints.Email\nimport org.springdoc.core.annotations.ParameterObject\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.web.PageableDefault\nimport org.springframework.validation.annotation.Validated\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PatchMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestBody\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RequestParam\nimport org.springframework.web.bind.annotation.RestController\n\n@SecurityRequirement(name = \"access-token\")\n@Tag(name = \"4. [호스트]\")\n@RestController\n@RequestMapping(\"/api/v1/hosts\")\n@Validated\nclass HostController(\n    private val readHostUseCase: ReadHostUseCase,\n    private val readHostsUseCase: ReadHostProfilesUseCase,\n    private val readHostEventsUseCase: ReadHostEventsUseCase,\n    private val readInviteUsersUseCase: ReadInviteUsersUseCase,\n    private val createHostUseCase: CreateHostUseCase,\n    private val updateHostProfileUseCase: UpdateHostProfileUseCase,\n    private val updateHostSlackUrlUseCase: UpdateHostSlackUrlUseCase,\n    private val updateHostUserRoleUseCase: UpdateHostUserRoleUseCase,\n    private val inviteHostUseCase: InviteHostUseCase,\n    private val joinHostUseCase: JoinHostUseCase,\n    private val rejectHostUseCase: RejectHostUseCase,\n    private val transferMasterUseCase: TransferMasterUseCase,\n) {\n    @Operation(summary = \"내가 속한 호스트 리스트를 가져옵니다.\")\n    @GetMapping\n    fun getAllHosts(\n        @CurrentUserId userId: Long,\n        @ParameterObject @PageableDefault(size = 10) pageable: Pageable\n    ): SliceResponse<HostProfileResponse> {\n        return readHostsUseCase.execute(userId, pageable)\n    }\n\n    @Operation(summary = \"고유 아이디에 해당하는 호스트 정보를 가져옵니다.\")\n    @GetMapping(\"/{hostId}\")\n    fun getHostById(@PathVariable hostId: Long): HostDetailResponse {\n        return readHostUseCase.execute(hostId)\n    }\n\n    @Operation(summary = \"해당 호스트에 가입하지 않은 유저를 이메일로 검색합니다.\")\n    @GetMapping(\"/{hostId}/invite/users\")\n    fun getInviteUserListByEmail(\n        @CurrentUserId userId: Long,\n        @PathVariable hostId: Long,\n        @RequestParam(value = \"email\") @Email email: String,\n    ): UserProfileVo {\n        return readInviteUsersUseCase.execute(userId, hostId, email)\n    }\n\n    @Operation(summary = \"해당 호스트가 관리중인 이벤트 리스트를 가져옵니다.\")\n    @GetMapping(\"/{hostId}/events\")\n    fun getHostEventsById(\n        @CurrentUserId userId: Long,\n        @PathVariable hostId: Long,\n        @ParameterObject @PageableDefault(size = 10) pageable: Pageable,\n    ): PageResponse<HostEventProfileResponse> {\n        return readHostEventsUseCase.execute(userId, hostId, pageable)\n    }\n\n    @Operation(summary = \"호스트 간편 생성. 호스트를 생성한 유저 자신은 마스터 호스트가 됩니다.\")\n    @PostMapping\n    fun createHost(@CurrentUserId userId: Long, @RequestBody @Valid createEventRequest: CreateHostRequest): HostResponse {\n        return createHostUseCase.execute(userId, createEventRequest)\n    }\n\n    @Operation(summary = \"초대 받은 호스트에 가입합니다.\")\n    @PostMapping(\"/{hostId}/join\")\n    fun joinHost(@CurrentUserId userId: Long, @PathVariable hostId: Long): HostDetailResponse {\n        return joinHostUseCase.execute(userId, hostId)\n    }\n\n    @Operation(summary = \"호스트 초대를 거절합니다.\")\n    @PostMapping(\"/{hostId}/reject\")\n    fun rejectHost(@CurrentUserId userId: Long, @PathVariable hostId: Long): HostDetailResponse {\n        return rejectHostUseCase.execute(userId, hostId)\n    }\n\n    @Operation(summary = \"다른 유저를 호스트 유저로 초대합니다.\")\n    @PostMapping(\"/{hostId}/invite\")\n    fun inviteHost(\n        @CurrentUserId userId: Long,\n        @PathVariable hostId: Long,\n        @RequestBody @Valid inviteHostRequest: InviteHostRequest,\n    ): HostDetailResponse {\n        return inviteHostUseCase.execute(userId, hostId, inviteHostRequest)\n    }\n\n    @Operation(summary = \"호스트 유저의 권한을 변경합니다. 매니저 이상만 가능합니다.\")\n    @PatchMapping(\"/{hostId}/role\")\n    fun patchHostUserRole(\n        @CurrentUserId userId: Long,\n        @PathVariable hostId: Long,\n        @RequestBody @Valid updateHostUserRoleRequest: UpdateHostUserRoleRequest,\n    ): HostDetailResponse {\n        return updateHostUserRoleUseCase.execute(userId, hostId, updateHostUserRoleRequest)\n    }\n\n    @Operation(summary = \"호스트 정보를 변경합니다. 매니저 이상만 가능합니다.\")\n    @PatchMapping(\"/{hostId}/profile\")\n    fun patchHostById(\n        @CurrentUserId userId: Long,\n        @PathVariable hostId: Long,\n        @RequestBody @Valid updateHostRequest: UpdateHostRequest,\n    ): HostDetailResponse {\n        return updateHostProfileUseCase.execute(userId, hostId, updateHostRequest)\n    }\n\n    @Operation(summary = \"호스트 슬랙 알람 URL 을 변경합니다. 매니저 이상만 가능합니다.\")\n    @PatchMapping(\"/{hostId}/slack\")\n    fun patchHostSlackUrlById(\n        @CurrentUserId userId: Long,\n        @PathVariable hostId: Long,\n        @RequestBody @Valid updateHostSlackRequest: UpdateHostSlackRequest,\n    ): HostDetailResponse {\n        return updateHostSlackUrlUseCase.execute(userId, hostId, updateHostSlackRequest)\n    }\n\n    @Operation(summary = \"호스트 마스터 권한을 다른 멤버에게 양도합니다. 마스터만 가능합니다.\")\n    @PostMapping(\"/{hostId}/transfer-master\")\n    fun transferMaster(\n        @CurrentUserId userId: Long,\n        @PathVariable hostId: Long,\n        @RequestBody @Valid request: TransferMasterRequest,\n    ): HostDetailResponse {\n        return transferMasterUseCase.execute(userId, hostId, request)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/model/dto/request/CreateHostRequest.kt",
    "content": "package band.gosrock.api.host.model.dto.request\n\nimport band.gosrock.common.annotation.Phone\nimport io.swagger.v3.oas.annotations.media.Schema\nimport jakarta.validation.constraints.Email\nimport jakarta.validation.constraints.NotBlank\nimport org.hibernate.validator.constraints.Length\n\n/** 호스트 간편 생성 요청 DTO */\ndata class CreateHostRequest(\n    @field:Schema(defaultValue = \"고스락\", description = \"호스트 이름\")\n    @field:NotBlank(message = \"호스트 이름을 입력해주세요\")\n    @field:Length(max = 15)\n    val name: String,\n\n    @field:Schema(defaultValue = \"gosrock@gsrk.com\", description = \"마스터 이메일\")\n    @field:Email(message = \"올바른 형식의 이메일을 입력하세요\")\n    val contactEmail: String,\n\n    @field:Schema(defaultValue = \"010-1111-3333\", description = \"마스터 전화번호\")\n    @field:Phone(message = \"올바른 형식의 번호를 입력하세요\")\n    val contactNumber: String,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/model/dto/request/InviteHostRequest.kt",
    "content": "package band.gosrock.api.host.model.dto.request\n\nimport band.gosrock.common.annotation.Enum\nimport band.gosrock.domain.domains.host.domain.HostRole\nimport io.swagger.v3.oas.annotations.media.Schema\nimport jakarta.validation.constraints.Email\n\n/** 호스트 초대 요청 DTO */\ndata class InviteHostRequest(\n    @field:Schema(defaultValue = \"kls1998@naver.com\", description = \"초대할 유저 이메일 주소\")\n    @field:Email(message = \"올바른 이메일을 입력해주세요\")\n    val email: String,\n\n    @field:Schema(defaultValue = \"MANAGER\", description = \"호스트 유저 역할\")\n    @field:Enum(message = \"MANAGER 또는 GUEST 만 허용됩니다\")\n    val role: HostRole,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/model/dto/request/TransferMasterRequest.kt",
    "content": "package band.gosrock.api.host.model.dto.request\n\nimport io.swagger.v3.oas.annotations.media.Schema\nimport jakarta.validation.constraints.NotNull\n\n/** 호스트 마스터 권한 양도 요청 DTO */\ndata class TransferMasterRequest(\n    @field:Schema(description = \"양도 대상 유저 아이디\")\n    @field:NotNull(message = \"양도 대상 유저 아이디를 입력해주세요\")\n    val newMasterUserId: Long,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/model/dto/request/UpdateHostRequest.kt",
    "content": "package band.gosrock.api.host.model.dto.request\n\nimport band.gosrock.common.annotation.Phone\nimport io.swagger.v3.oas.annotations.media.Schema\nimport jakarta.validation.constraints.Email\nimport jakarta.validation.constraints.NotEmpty\nimport jakarta.validation.constraints.NotNull\n\n/** 호스트 정보 변경 요청 DTO */\ndata class UpdateHostRequest(\n    @field:Schema(defaultValue = \"test/host/5/aa.jpg\", description = \"호스트 프로필 이미지\")\n    @field:NotEmpty\n    val profileImageKey: String,\n\n    @field:Schema(defaultValue = \"고슬고슬고스락\", description = \"호스트 간단 소개\")\n    @field:NotNull(message = \"간단 소개를 입력해주세요\")\n    val introduce: String,\n\n    @field:Schema(defaultValue = \"010-1111-3333\", description = \"마스터 전화번호\")\n    @field:Phone(message = \"올바른 형식의 번호를 입력하세요\")\n    val contactNumber: String,\n\n    @field:Schema(defaultValue = \"gosrock@gsrk.com\", description = \"마스터 이메일\")\n    @field:Email(message = \"올바른 형식의 이메일을 입력하세요\")\n    val contactEmail: String,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/model/dto/request/UpdateHostSlackRequest.kt",
    "content": "package band.gosrock.api.host.model.dto.request\n\nimport io.swagger.v3.oas.annotations.media.Schema\nimport jakarta.validation.constraints.NotBlank\nimport org.hibernate.validator.constraints.URL\n\n/** 호스트 슬랙 알람 URL 수정 요청 DTO */\ndata class UpdateHostSlackRequest(\n    @field:Schema(defaultValue = \"https://slack.dd.com\", description = \"슬랙 웹훅 URL\")\n    @field:NotBlank(message = \"올바른 슬랙 URL 을 입력해주세요\")\n    @field:URL(message = \"올바른 슬랙 URL 을 입력해주세요\")\n    val slackUrl: String,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/model/dto/request/UpdateHostUserRoleRequest.kt",
    "content": "package band.gosrock.api.host.model.dto.request\n\nimport band.gosrock.common.annotation.Enum\nimport band.gosrock.domain.domains.host.domain.HostRole\nimport io.swagger.v3.oas.annotations.media.Schema\nimport jakarta.validation.constraints.Positive\n\n/** 호스트 유저 역할 수정 요청 DTO */\ndata class UpdateHostUserRoleRequest(\n    @field:Schema(defaultValue = \"1\", description = \"호스트 유저 아이디\")\n    @field:Positive(message = \"올바른 유저 고유 아이디를 입력해주세요\")\n    val userId: Long,\n\n    @field:Schema(defaultValue = \"MANAGER\", description = \"호스트 유저 역할\")\n    @field:Enum(message = \"GUEST, MANAGER, MASTER 만 허용됩니다\")\n    val role: HostRole,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/model/dto/response/HostDetailResponse.kt",
    "content": "package band.gosrock.api.host.model.dto.response\n\nimport band.gosrock.domain.common.vo.HostInfoVo\nimport band.gosrock.domain.common.vo.HostUserVo\nimport band.gosrock.domain.domains.host.domain.Host\nimport com.fasterxml.jackson.annotation.JsonUnwrapped\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class HostDetailResponse(\n    @field:Schema(description = \"호스트 정보\")\n    @field:JsonUnwrapped\n    val hostInfo: HostInfoVo,\n\n    @field:Schema(description = \"마스터 유저의 정보\")\n    val masterUser: HostUserVo,\n\n    @field:Schema(description = \"호스트 유저 정보\")\n    val hostUsers: List<HostUserVo>,\n\n    @field:Schema(description = \"슬랙 알람 url\")\n    val slackUrl: String?,\n\n    @field:Schema(description = \"파트너쉽 여부\")\n    val partner: Boolean,\n) {\n    companion object {\n        fun of(host: Host, hostUserVoSet: List<HostUserVo>): HostDetailResponse {\n            var masterUser: HostUserVo? = null\n            val hostUserVoList = mutableListOf<HostUserVo>()\n\n            hostUserVoSet.forEach { hostUserVo ->\n                if (hostUserVo.userInfoVo.userId == host.masterUserId) {\n                    masterUser = hostUserVo\n                } else {\n                    hostUserVoList.add(hostUserVo)\n                }\n            }\n\n            return HostDetailResponse(\n                hostInfo = HostInfoVo.from(host),\n                masterUser = masterUser!!,\n                hostUsers = hostUserVoList,\n                partner = host.partner,\n                slackUrl = host.slackUrl,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/model/dto/response/HostEventProfileResponse.kt",
    "content": "package band.gosrock.api.host.model.dto.response\n\nimport band.gosrock.domain.common.vo.EventProfileVo\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.host.domain.Host\nimport com.fasterxml.jackson.annotation.JsonUnwrapped\n\ndata class HostEventProfileResponse(\n    val hostId: Long?,\n    val hostName: String?,\n    @field:JsonUnwrapped val eventProfileVo: EventProfileVo,\n) {\n    companion object {\n        fun of(host: Host, event: Event): HostEventProfileResponse {\n            return HostEventProfileResponse(\n                hostId = host.id,\n                hostName = host.profile!!.name,\n                eventProfileVo = event.toEventProfileVo(),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/model/dto/response/HostProfileResponse.kt",
    "content": "package band.gosrock.api.host.model.dto.response\n\nimport band.gosrock.domain.common.vo.ImageVo\nimport band.gosrock.domain.domains.host.domain.Host\nimport band.gosrock.domain.domains.host.domain.HostRole\nimport io.swagger.v3.oas.annotations.media.Schema\n\n/** 내가 속한 호스트의 간략한 정보에 대한 응답 DTO */\ndata class HostProfileResponse(\n    @field:Schema(description = \"호스트 고유 아이디\")\n    val hostId: Long?,\n\n    @field:Schema(description = \"호스트 이름\")\n    val name: String?,\n\n    @field:Schema(description = \"호스트 한줄 소개\")\n    val introduce: String?,\n\n    @field:Schema(description = \"호스트 프로필 이미지\")\n    val profileImage: ImageVo?,\n\n    @field:Schema(description = \"속한 호스트에서의 역할\")\n    val role: HostRole,\n\n    @field:Schema(description = \"이 호스트의 마스터인지 여부\")\n    val isMaster: Boolean,\n\n    @field:Schema(description = \"이 호스트 초대를 수락했는지 여부\")\n    val active: Boolean?,\n) {\n    companion object {\n        fun of(host: Host, userId: Long): HostProfileResponse {\n            val hostUser = host.getHostUserByUserId(userId)\n            return HostProfileResponse(\n                hostId = host.id,\n                name = host.profile!!.name,\n                introduce = host.profile!!.introduce,\n                profileImage = host.profile!!.profileImage,\n                role = hostUser.role,\n                isMaster = host.masterUserId == userId,\n                active = hostUser.active,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/model/dto/response/HostResponse.kt",
    "content": "package band.gosrock.api.host.model.dto.response\n\nimport band.gosrock.domain.common.vo.HostInfoVo\nimport band.gosrock.domain.domains.host.domain.Host\nimport com.fasterxml.jackson.annotation.JsonUnwrapped\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class HostResponse(\n    @field:Schema(description = \"호스트 프로필\")\n    @field:JsonUnwrapped\n    val profile: HostInfoVo,\n\n    @field:Schema(description = \"마스터 유저의 고유 아이디\")\n    val masterUserId: Long?,\n\n    @field:Schema(description = \"파트너쉽 여부\")\n    val partner: Boolean,\n) {\n    companion object {\n        fun of(host: Host): HostResponse {\n            return HostResponse(\n                profile = HostInfoVo.from(host),\n                masterUserId = host.masterUserId,\n                partner = host.partner,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/model/mapper/HostMapper.kt",
    "content": "package band.gosrock.api.host.model.mapper\n\nimport band.gosrock.api.host.model.dto.request.CreateHostRequest\nimport band.gosrock.api.host.model.dto.request.UpdateHostRequest\nimport band.gosrock.api.host.model.dto.response.HostDetailResponse\nimport band.gosrock.common.annotation.Mapper\nimport band.gosrock.domain.common.vo.HostUserVo\nimport band.gosrock.domain.common.vo.UserInfoVo\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.domain.Host\nimport band.gosrock.domain.domains.host.domain.HostProfile\nimport band.gosrock.domain.domains.host.domain.HostRole\nimport band.gosrock.domain.domains.host.domain.HostUser\nimport band.gosrock.domain.domains.host.exception.AlreadyJoinedHostException\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@Mapper\n@Transactional(readOnly = true)\nclass HostMapper(\n    private val hostAdaptor: HostAdaptor,\n    private val userAdaptor: UserAdaptor,\n) {\n    fun toEntity(createHostRequest: CreateHostRequest, masterUserId: Long): Host {\n        return Host(\n            name = createHostRequest.name,\n            contactEmail = createHostRequest.contactEmail,\n            contactNumber = createHostRequest.contactNumber,\n            masterUserId = masterUserId,\n        )\n    }\n\n    fun toHostProfile(updateHostRequest: UpdateHostRequest): HostProfile {\n        return HostProfile(\n            introduce = updateHostRequest.introduce,\n            profileImageKey = updateHostRequest.profileImageKey,\n            contactEmail = updateHostRequest.contactEmail,\n            contactNumber = updateHostRequest.contactNumber,\n        )\n    }\n\n    /** 호스트 역할을 지정하여 주입하는 생성자 */\n    fun toHostUser(hostId: Long, userId: Long, role: HostRole): HostUser {\n        val host = hostAdaptor.findById(hostId)\n        return HostUser(host = host, userId = userId, role = role)\n    }\n\n    /** 매니저로 주입하는 생성자 */\n    fun toManagerHostUser(hostId: Long, userId: Long): HostUser {\n        val host = hostAdaptor.findById(hostId)\n        return HostUser(host = host, userId = userId, role = HostRole.MANAGER)\n    }\n\n    /** 마스터 주입하는 생성자 */\n    fun toMasterHostUser(hostId: Long, userId: Long): HostUser {\n        val host = hostAdaptor.findById(hostId)\n        return HostUser(host = host, userId = userId, role = HostRole.MASTER)\n    }\n\n    fun toHostInviteUserList(hostId: Long, email: String) =\n        userAdaptor.queryUserByEmail(email).let { inviteUser ->\n            val host = hostAdaptor.findById(hostId)\n            if (host.hasHostUserId(inviteUser.id!!)) {\n                throw AlreadyJoinedHostException.EXCEPTION\n            }\n            inviteUser.toUserProfileVo()\n        }\n\n    fun toHostDetailResponse(hostId: Long): HostDetailResponse {\n        val host = hostAdaptor.findById(hostId)\n        return toHostDetailResponseExecute(host)\n    }\n\n    fun toHostDetailResponse(host: Host): HostDetailResponse {\n        return toHostDetailResponseExecute(host)\n    }\n\n    private fun toHostDetailResponseExecute(host: Host): HostDetailResponse {\n        val userIds = host.getHostUser_UserIds()\n        val userList = userAdaptor.queryUserListByIdIn(userIds)\n        val userMap = userList.associateBy { it.id }\n        val hostUserVoList = mutableListOf<HostUserVo>()\n\n        for (userId in userIds) {\n            val user = userMap[userId]\n            if (user != null) {\n                val userInfoVo: UserInfoVo = user.toUserInfoVo()\n                val hostUser = host.getHostUserByUserId(userId)\n                val hostUserVo = HostUserVo.from(userInfoVo, hostUser)\n                hostUserVoList.add(hostUserVo)\n            }\n        }\n\n        return HostDetailResponse.of(host, hostUserVoList)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/service/CreateHostUseCase.kt",
    "content": "package band.gosrock.api.host.service\n\nimport band.gosrock.api.host.model.dto.request.CreateHostRequest\nimport band.gosrock.api.host.model.dto.response.HostResponse\nimport band.gosrock.api.host.model.mapper.HostMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.host.service.HostService\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.slf4j.LoggerFactory\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass CreateHostUseCase(\n    private val userAdaptor: UserAdaptor,\n    private val hostService: HostService,\n    private val hostMapper: HostMapper,\n) {\n    private val log = LoggerFactory.getLogger(CreateHostUseCase::class.java)\n\n    @Transactional\n    fun execute(userId: Long, createHostRequest: CreateHostRequest): HostResponse {\n        log.info(\"[CreateHostUseCase][execute] 호스트 생성 userId={}\", userId)\n        // 존재하는 유저인지 검증\n        userAdaptor.queryUser(userId)\n        // 호스트 생성\n        val host = hostService.createHost(hostMapper.toEntity(createHostRequest, userId))\n        // 생성한 유저를 마스터 권한으로 등록\n        val masterHostUser = hostMapper.toMasterHostUser(host.id!!, userId)\n        // 초대 보류 없이 즉시 활성화\n        masterHostUser.activate()\n        return HostResponse.of(hostService.addHostUser(host, masterHostUser))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/service/InviteHostUseCase.kt",
    "content": "package band.gosrock.api.host.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.HOST_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.MANAGER\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.host.model.dto.request.InviteHostRequest\nimport band.gosrock.api.host.model.dto.response.HostDetailResponse\nimport band.gosrock.api.host.model.mapper.HostMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.service.HostService\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\n\n@UseCase\nclass InviteHostUseCase(\n    private val userAdaptor: UserAdaptor,\n    private val hostService: HostService,\n    private val hostAdaptor: HostAdaptor,\n    private val hostMapper: HostMapper,\n) {\n    @HostRolesAllowed(role = MANAGER, findHostFrom = HOST_ID)\n    fun execute(userId: Long, hostId: Long, inviteHostRequest: InviteHostRequest): HostDetailResponse {\n        val host = hostAdaptor.findById(hostId)\n        val invitedUser = userAdaptor.queryUserByEmail(inviteHostRequest.email)\n        val invitedUserId = invitedUser.id!!\n        val role = inviteHostRequest.role\n        val hostUser = hostMapper.toHostUser(hostId, invitedUserId, role)\n\n        return hostMapper.toHostDetailResponse(hostService.inviteHostUser(host, hostUser))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/service/JoinHostUseCase.kt",
    "content": "package band.gosrock.api.host.service\n\nimport band.gosrock.api.host.model.dto.response.HostDetailResponse\nimport band.gosrock.api.host.model.mapper.HostMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.service.HostService\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass JoinHostUseCase(\n    private val hostService: HostService,\n    private val hostAdaptor: HostAdaptor,\n    private val hostMapper: HostMapper,\n) {\n    @Transactional\n    fun execute(userId: Long, hostId: Long): HostDetailResponse {\n        val host = hostAdaptor.findById(hostId)\n\n        return hostMapper.toHostDetailResponse(hostService.activateHostUser(host, userId))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/service/ReadHostEventsUseCase.kt",
    "content": "package band.gosrock.api.host.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.HOST_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.GUEST\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.common.page.PageResponse\nimport band.gosrock.api.host.model.dto.response.HostEventProfileResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport org.springframework.data.domain.Pageable\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass ReadHostEventsUseCase(\n    private val hostAdaptor: HostAdaptor,\n    private val eventAdaptor: EventAdaptor,\n) {\n    @HostRolesAllowed(role = GUEST, findHostFrom = HOST_ID)\n    fun execute(userId: Long, hostId: Long, pageable: Pageable): PageResponse<HostEventProfileResponse> {\n        val host = hostAdaptor.findById(hostId)\n        return PageResponse.of(\n            eventAdaptor\n                .findAllByHostId(hostId, pageable)\n                .map { event -> HostEventProfileResponse.of(host, event) }\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/service/ReadHostProfilesUseCase.kt",
    "content": "package band.gosrock.api.host.service\n\nimport band.gosrock.api.common.slice.SliceResponse\nimport band.gosrock.api.host.model.dto.response.HostProfileResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport org.springframework.data.domain.Pageable\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass ReadHostProfilesUseCase(\n    private val hostAdaptor: HostAdaptor,\n) {\n    @Transactional(readOnly = true)\n    fun execute(userId: Long, pageable: Pageable): SliceResponse<HostProfileResponse> {\n        return SliceResponse.of(\n            hostAdaptor\n                .querySliceHostsByUserId(userId, pageable)\n                .map { host -> HostProfileResponse.of(host, userId) }\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/service/ReadHostUseCase.kt",
    "content": "package band.gosrock.api.host.service\n\nimport band.gosrock.api.host.model.dto.response.HostDetailResponse\nimport band.gosrock.api.host.model.mapper.HostMapper\nimport band.gosrock.common.annotation.UseCase\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass ReadHostUseCase(\n    private val hostMapper: HostMapper,\n) {\n    @Transactional(readOnly = true)\n    fun execute(hostId: Long): HostDetailResponse {\n        return hostMapper.toHostDetailResponse(hostId)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/service/ReadInviteUsersUseCase.kt",
    "content": "package band.gosrock.api.host.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.HOST_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.GUEST\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.host.model.mapper.HostMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.common.vo.UserProfileVo\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass ReadInviteUsersUseCase(\n    private val hostMapper: HostMapper,\n) {\n    @Transactional(readOnly = true)\n    @HostRolesAllowed(role = GUEST, findHostFrom = HOST_ID)\n    fun execute(userId: Long, hostId: Long, email: String): UserProfileVo {\n        return hostMapper.toHostInviteUserList(hostId, email)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/service/RejectHostUseCase.kt",
    "content": "package band.gosrock.api.host.service\n\nimport band.gosrock.api.host.model.dto.response.HostDetailResponse\nimport band.gosrock.api.host.model.mapper.HostMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.service.HostService\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass RejectHostUseCase(\n    private val hostService: HostService,\n    private val hostAdaptor: HostAdaptor,\n    private val hostMapper: HostMapper,\n) {\n    @Transactional\n    fun execute(userId: Long, hostId: Long): HostDetailResponse {\n        val host = hostAdaptor.findById(hostId)\n\n        return hostMapper.toHostDetailResponse(hostService.removeHostUser(host, userId))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/service/TransferMasterUseCase.kt",
    "content": "package band.gosrock.api.host.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.HOST_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.MASTER\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.host.model.dto.request.TransferMasterRequest\nimport band.gosrock.api.host.model.dto.response.HostDetailResponse\nimport band.gosrock.api.host.model.mapper.HostMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.repository.HostRepository\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass TransferMasterUseCase(\n    private val hostAdaptor: HostAdaptor,\n    private val hostRepository: HostRepository,\n    private val hostMapper: HostMapper,\n) {\n    @Transactional\n    @HostRolesAllowed(role = MASTER, findHostFrom = HOST_ID)\n    fun execute(userId: Long, hostId: Long, request: TransferMasterRequest): HostDetailResponse {\n        val host = hostAdaptor.findById(hostId)\n        host.transferMaster(userId, request.newMasterUserId)\n        hostRepository.save(host)\n        return hostMapper.toHostDetailResponse(host)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/service/UpdateHostProfileUseCase.kt",
    "content": "package band.gosrock.api.host.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.HOST_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.MANAGER\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.host.model.dto.request.UpdateHostRequest\nimport band.gosrock.api.host.model.dto.response.HostDetailResponse\nimport band.gosrock.api.host.model.mapper.HostMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.service.HostService\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass UpdateHostProfileUseCase(\n    private val hostService: HostService,\n    private val hostAdaptor: HostAdaptor,\n    private val hostMapper: HostMapper,\n) {\n    @Transactional\n    @HostRolesAllowed(role = MANAGER, findHostFrom = HOST_ID)\n    fun execute(userId: Long, hostId: Long, updateHostRequest: UpdateHostRequest): HostDetailResponse {\n        val host = hostAdaptor.findById(hostId)\n\n        return hostMapper.toHostDetailResponse(\n            hostService.updateHostProfile(host, hostMapper.toHostProfile(updateHostRequest))\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/service/UpdateHostSlackUrlUseCase.kt",
    "content": "package band.gosrock.api.host.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.HOST_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.MANAGER\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.host.model.dto.request.UpdateHostSlackRequest\nimport band.gosrock.api.host.model.dto.response.HostDetailResponse\nimport band.gosrock.api.host.model.mapper.HostMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.exception.InvalidSlackUrlException\nimport band.gosrock.domain.domains.host.service.HostService\nimport band.gosrock.infrastructure.config.slack.SlackMessageProvider\nimport java.net.UnknownHostException\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass UpdateHostSlackUrlUseCase(\n    private val hostService: HostService,\n    private val hostAdaptor: HostAdaptor,\n    private val hostMapper: HostMapper,\n    private val slackMessageProvider: SlackMessageProvider,\n) {\n    @Transactional\n    @HostRolesAllowed(role = MANAGER, findHostFrom = HOST_ID)\n    fun execute(userId: Long, hostId: Long, updateHostSlackRequest: UpdateHostSlackRequest): HostDetailResponse {\n        val host = hostAdaptor.findById(hostId)\n        val slackUrl = updateHostSlackRequest.slackUrl\n        hostService.validateDuplicatedSlackUrl(host, slackUrl)\n\n        return try {\n            slackMessageProvider.register(slackUrl)\n            hostMapper.toHostDetailResponse(hostService.updateHostSlackUrl(host, slackUrl))\n        } catch (e: UnknownHostException) {\n            throw InvalidSlackUrlException.EXCEPTION\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/host/service/UpdateHostUserRoleUseCase.kt",
    "content": "package band.gosrock.api.host.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.HOST_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.MANAGER\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.host.model.dto.request.UpdateHostUserRoleRequest\nimport band.gosrock.api.host.model.dto.response.HostDetailResponse\nimport band.gosrock.api.host.model.mapper.HostMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.service.HostService\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass UpdateHostUserRoleUseCase(\n    private val hostService: HostService,\n    private val hostAdaptor: HostAdaptor,\n    private val hostMapper: HostMapper,\n) {\n    @Transactional\n    @HostRolesAllowed(role = MANAGER, findHostFrom = HOST_ID)\n    fun execute(userId: Long, hostId: Long, updateHostUserRoleRequest: UpdateHostUserRoleRequest): HostDetailResponse {\n        val host = hostAdaptor.findById(hostId)\n        val updateUserId = updateHostUserRoleRequest.userId\n        val updateUserRole = updateHostUserRoleRequest.role\n\n        return hostMapper.toHostDetailResponse(\n            hostService.updateHostUserRole(host, updateUserId, updateUserRole)\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/image/controller/ImageController.kt",
    "content": "package band.gosrock.api.image.controller\n\nimport band.gosrock.api.image.dto.ImageUrlResponse\nimport band.gosrock.api.image.service.GetImageUploadUrlUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport band.gosrock.infrastructure.config.s3.ImageFileExtension\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RequestParam\nimport org.springframework.web.bind.annotation.RestController\n\n@SecurityRequirement(name = \"access-token\")\n@Tag(name = \"a1. [이미지]\")\n@RestController\n@RequestMapping(\"/api/v1\")\nclass ImageController(\n    private val getImageUploadUrlUseCase: GetImageUploadUrlUseCase,\n) {\n    @Operation(summary = \"이벤트 관련 이미지 업로드 url 요청할수 있는 api 입니다.\")\n    @PostMapping(value = [\"/events/{eventId}/images\"])\n    fun getIssuedTickets(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @RequestParam imageFileExtension: ImageFileExtension,\n    ): ImageUrlResponse = getImageUploadUrlUseCase.forEvent(userId, eventId, imageFileExtension)\n\n    @Operation(summary = \"호스트 관련 이미지 업로드 url 요청할수 있는 api 입니다.\")\n    @PostMapping(value = [\"/hosts/{hostId}/images\"])\n    fun patchIssuedTicketStatus(\n        @CurrentUserId userId: Long,\n        @PathVariable hostId: Long,\n        @RequestParam imageFileExtension: ImageFileExtension,\n    ): ImageUrlResponse = getImageUploadUrlUseCase.forHost(userId, hostId, imageFileExtension)\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/image/dto/ImageUrlRequest.kt",
    "content": "package band.gosrock.api.image.dto\n\nimport band.gosrock.common.annotation.Enum\nimport band.gosrock.infrastructure.config.s3.ImageFileExtension\n\ndata class ImageUrlRequest(\n    @field:Enum\n    val imageFileExtension: ImageFileExtension,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/image/dto/ImageUrlResponse.kt",
    "content": "package band.gosrock.api.image.dto\n\nimport band.gosrock.domain.common.vo.ImageVo\nimport band.gosrock.infrastructure.config.s3.ImageUrlDto\n\ndata class ImageUrlResponse(\n    val presignedUrl: String,\n    val key: String,\n    val url: ImageVo,\n) {\n    companion object {\n        @JvmStatic\n        fun from(urlDto: ImageUrlDto): ImageUrlResponse =\n            ImageUrlResponse(\n                presignedUrl = urlDto.url,\n                key = urlDto.key,\n                url = ImageVo.valueOf(urlDto.key),\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/image/service/GetImageUploadUrlUseCase.kt",
    "content": "package band.gosrock.api.image.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.HOST_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.MANAGER\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.image.dto.ImageUrlResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.infrastructure.config.s3.ImageFileExtension\nimport band.gosrock.infrastructure.config.s3.S3UploadPresignedUrlService\n\n@UseCase\nclass GetImageUploadUrlUseCase(\n    private val presignedUrlService: S3UploadPresignedUrlService,\n) {\n    @HostRolesAllowed(role = MANAGER, findHostFrom = EVENT_ID)\n    fun forEvent(userId: Long, eventId: Long, imageFileExtension: ImageFileExtension): ImageUrlResponse =\n        ImageUrlResponse.from(presignedUrlService.forEvent(eventId, imageFileExtension))\n\n    @HostRolesAllowed(role = MANAGER, findHostFrom = HOST_ID)\n    fun forHost(userId: Long, hostId: Long, imageFileExtension: ImageFileExtension): ImageUrlResponse =\n        ImageUrlResponse.from(presignedUrlService.forHost(hostId, imageFileExtension))\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/issuedTicket/controller/AdminIssuedTicketController.kt",
    "content": "package band.gosrock.api.issuedTicket.controller\n\nimport band.gosrock.api.common.page.PageResponse\nimport band.gosrock.api.issuedTicket.dto.request.AdminIssuedTicketTableQueryRequest\nimport band.gosrock.api.issuedTicket.dto.response.IssuedTicketAdminTableElement\nimport band.gosrock.api.issuedTicket.service.EntranceIssuedTicketUseCase\nimport band.gosrock.api.issuedTicket.service.ReadIssuedTicketsUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport band.gosrock.domain.common.vo.IssuedTicketInfoVo\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport org.springdoc.core.annotations.ParameterObject\nimport org.springframework.data.domain.Pageable\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PatchMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RestController\n\n@SecurityRequirement(name = \"access-token\")\n@Tag(name = \"8-1. [이벤트관리] 발급 티켓 관리 \")\n@RestController\n@RequestMapping(\"/api/v1/events/{eventId}/issuedTickets\")\nclass AdminIssuedTicketController(\n    private val readIssuedTicketsUseCase: ReadIssuedTicketsUseCase,\n    private val entranceIssuedTicketUseCase: EntranceIssuedTicketUseCase,\n) {\n\n    @Operation(summary = \"[어드민 기능] 발급 티켓 리스트 가져오기 API 입니다.\")\n    @GetMapping\n    fun getIssuedTickets(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @ParameterObject queryRequest: AdminIssuedTicketTableQueryRequest,\n        @ParameterObject pageable: Pageable,\n    ): PageResponse<IssuedTicketAdminTableElement> {\n        return readIssuedTicketsUseCase.execute(userId, pageable, eventId, queryRequest)\n    }\n\n    @Operation(summary = \"[어드민 기능] 발급 티켓 입장 처리 API 입니다.\")\n    @PatchMapping(value = [\"/{uuid}\"])\n    fun patchIssuedTicketStatus(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @PathVariable uuid: String,\n    ): IssuedTicketInfoVo {\n        return entranceIssuedTicketUseCase.execute(userId, eventId, uuid)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/issuedTicket/controller/IssuedTicketController.kt",
    "content": "package band.gosrock.api.issuedTicket.controller\n\nimport band.gosrock.common.annotation.CurrentUserId\nimport band.gosrock.api.issuedTicket.dto.response.RetrieveIssuedTicketDetailResponse\nimport band.gosrock.api.issuedTicket.service.ReadIssuedTicketUseCase\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RestController\n\n@SecurityRequirement(name = \"access-token\")\n@Tag(name = \"8-2. [발급티켓]\")\n@RestController\n@RequestMapping(\"/api/v1/issuedTickets\")\nclass IssuedTicketController(\n    private val readIssuedTicketUseCase: ReadIssuedTicketUseCase,\n) {\n\n    @Operation(summary = \"발급 티켓 가져오기 API 입니다.\")\n    @GetMapping(value = [\"/{uuid}\"], produces = [\"application/json; charset=utf-8\"])\n    fun getIssuedTicket(@CurrentUserId userId: Long, @PathVariable uuid: String): RetrieveIssuedTicketDetailResponse {\n        return readIssuedTicketUseCase.execute(userId, uuid)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/issuedTicket/dto/request/AdminIssuedTicketTableQueryRequest.kt",
    "content": "package band.gosrock.api.issuedTicket.dto.request\n\nimport band.gosrock.domain.domains.issuedTicket.repository.condition.FindEventIssuedTicketsCondition\nimport band.gosrock.domain.domains.order.repository.condition.AdminTableSearchType\nimport com.fasterxml.jackson.annotation.JsonIgnore\nimport io.swagger.v3.oas.annotations.media.Schema\n\nclass AdminIssuedTicketTableQueryRequest(\n    val searchType: AdminTableSearchType?,\n    @Schema(nullable = true)\n    val searchString: String?,\n) {\n    @JsonIgnore\n    fun toCondition(eventId: Long): FindEventIssuedTicketsCondition {\n        return FindEventIssuedTicketsCondition(\n            eventId = eventId,\n            searchString = searchString,\n            searchType = searchType,\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/issuedTicket/dto/response/IssuedTicketAdminTableElement.kt",
    "content": "package band.gosrock.api.issuedTicket.dto.response\n\nimport band.gosrock.domain.common.vo.IssuedTicketInfoVo\nimport band.gosrock.domain.common.vo.IssuedTicketOptionAnswerVo\nimport band.gosrock.domain.common.vo.UserInfoVo\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketOptionAnswer\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.user.domain.User\nimport com.fasterxml.jackson.annotation.JsonUnwrapped\n\ndata class IssuedTicketAdminTableElement(\n    @JsonUnwrapped val issuedTicketInfo: IssuedTicketInfoVo,\n    val userInfo: UserInfoVo,\n    val orderNo: String?,\n    val issuedTicketOptionAnswers: List<IssuedTicketOptionAnswerVo>,\n) {\n    companion object {\n        @JvmStatic\n        fun of(issuedTicket: IssuedTicket, user: User, order: Order): IssuedTicketAdminTableElement {\n            return IssuedTicketAdminTableElement(\n                issuedTicketInfo = issuedTicket.toIssuedTicketInfoVo(),\n                userInfo = user.toUserInfoVo(),\n                orderNo = order.orderNo,\n                issuedTicketOptionAnswers = issuedTicket.issuedTicketOptionAnswers\n                    .map(IssuedTicketOptionAnswer::toIssuedTicketOptionAnswerVo),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/issuedTicket/dto/response/RetrieveIssuedTicketDTO.kt",
    "content": "package band.gosrock.api.issuedTicket.dto.response\n\nimport band.gosrock.domain.common.vo.IssuedTicketInfoVo\nimport band.gosrock.domain.common.vo.IssuedTicketOptionAnswerVo\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketOptionAnswer\n\ndata class RetrieveIssuedTicketDTO(\n    val issuedTicketInfo: IssuedTicketInfoVo,\n    val issuedTicketOptionAnswers: List<IssuedTicketOptionAnswerVo>,\n) {\n    companion object {\n        @JvmStatic\n        fun of(issuedTicket: IssuedTicket): RetrieveIssuedTicketDTO {\n            return RetrieveIssuedTicketDTO(\n                issuedTicketInfo = issuedTicket.toIssuedTicketInfoVo(),\n                issuedTicketOptionAnswers = issuedTicket.issuedTicketOptionAnswers\n                    .map(IssuedTicketOptionAnswer::toIssuedTicketOptionAnswerVo),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/issuedTicket/dto/response/RetrieveIssuedTicketDetailResponse.kt",
    "content": "package band.gosrock.api.issuedTicket.dto.response\n\nimport band.gosrock.domain.common.vo.EventInfoVo\nimport band.gosrock.domain.common.vo.IssuedTicketInfoVo\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\n\ndata class RetrieveIssuedTicketDetailResponse(\n    val issuedTicketInfo: IssuedTicketInfoVo,\n    val eventInfo: EventInfoVo,\n) {\n    companion object {\n        @JvmStatic\n        fun of(issuedTicket: IssuedTicket, event: Event): RetrieveIssuedTicketDetailResponse {\n            return RetrieveIssuedTicketDetailResponse(\n                issuedTicketInfo = issuedTicket.toIssuedTicketInfoVo(),\n                eventInfo = event.toEventInfoVo(),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/issuedTicket/dto/response/RetrieveIssuedTicketListResponse.kt",
    "content": "package band.gosrock.api.issuedTicket.dto.response\n\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport org.springframework.data.domain.Page\n\ndata class RetrieveIssuedTicketListResponse(\n    val page: Int,\n    val totalPage: Int,\n    val issuedTickets: List<RetrieveIssuedTicketDTO>,\n) {\n    companion object {\n        @JvmStatic\n        fun of(issuedTickets: Page<IssuedTicket>): RetrieveIssuedTicketListResponse {\n            return RetrieveIssuedTicketListResponse(\n                page = issuedTickets.pageable.pageNumber,\n                totalPage = issuedTickets.totalPages,\n                issuedTickets = issuedTickets.map(RetrieveIssuedTicketDTO::of).toList(),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/issuedTicket/mapper/IssuedTicketMapper.kt",
    "content": "package band.gosrock.api.issuedTicket.mapper\n\nimport band.gosrock.api.common.page.PageResponse\nimport band.gosrock.api.issuedTicket.dto.response.IssuedTicketAdminTableElement\nimport band.gosrock.api.issuedTicket.dto.response.RetrieveIssuedTicketDetailResponse\nimport band.gosrock.common.annotation.Mapper\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.issuedTicket.adaptor.IssuedTicketAdaptor\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport band.gosrock.domain.domains.issuedTicket.repository.condition.FindEventIssuedTicketsCondition\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.domain.domains.user.domain.User\nimport org.springframework.data.domain.Pageable\nimport org.springframework.transaction.annotation.Transactional\n\n@Mapper\nclass IssuedTicketMapper(\n    private val issuedTicketAdaptor: IssuedTicketAdaptor,\n    private val eventAdaptor: EventAdaptor,\n    private val userAdaptor: UserAdaptor,\n    private val orderAdaptor: OrderAdaptor,\n) {\n\n    @Transactional(readOnly = true)\n    fun toIssuedTicketAdminTableElementPageResponse(\n        page: Pageable,\n        condition: FindEventIssuedTicketsCondition,\n    ): PageResponse<IssuedTicketAdminTableElement> {\n        val issuedTickets = issuedTicketAdaptor.searchIssuedTicket(page, condition)\n        val orderUuids = issuedTickets.mapNotNull { it.orderUuid }.distinct()\n        val userIds = issuedTickets.mapNotNull { it.getUserId() }.distinct()\n        val users = userAdaptor.findUserByIdIn(userIds)\n        val orders = orderAdaptor.findByUuidIn(orderUuids)\n        val issuedTicketAdminTableElements = issuedTickets.map { issuedTicket ->\n            IssuedTicketAdminTableElement.of(\n                issuedTicket,\n                getUser(users, issuedTicket.getUserId()),\n                getOrder(orders, issuedTicket.orderUuid),\n            )\n        }\n        return PageResponse.of(issuedTicketAdminTableElements)\n    }\n\n    private fun getOrder(orders: List<Order>, orderUuid: String?): Order {\n        return orders.first { it.uuid == orderUuid }\n    }\n\n    private fun getUser(users: List<User>, userId: Long?): User {\n        return users.first { it.id == userId }\n    }\n\n    @Transactional(readOnly = true)\n    fun toIssuedTicketDetailResponse(\n        currentUserId: Long,\n        uuid: String,\n    ): RetrieveIssuedTicketDetailResponse {\n        val issuedTicket = issuedTicketAdaptor.findForUser(currentUserId, uuid)\n        val event = eventAdaptor.findById(issuedTicket.eventId!!)\n        return RetrieveIssuedTicketDetailResponse.of(issuedTicket, event)\n    }\n\n    @Transactional(readOnly = true)\n    fun getIssuedTicket(issuedTicketId: Long): IssuedTicket {\n        return issuedTicketAdaptor.queryIssuedTicket(issuedTicketId)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/issuedTicket/service/EntranceIssuedTicketUseCase.kt",
    "content": "package band.gosrock.api.issuedTicket.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.MANAGER\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.common.vo.IssuedTicketInfoVo\nimport band.gosrock.domain.domains.issuedTicket.service.IssuedTicketDomainService\nimport org.slf4j.LoggerFactory\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass EntranceIssuedTicketUseCase(\n    private val issuedTicketDomainService: IssuedTicketDomainService,\n) {\n    private val log = LoggerFactory.getLogger(EntranceIssuedTicketUseCase::class.java)\n\n    @Transactional\n    @HostRolesAllowed(role = MANAGER, findHostFrom = EVENT_ID)\n    fun execute(userId: Long, eventId: Long, uuid: String): IssuedTicketInfoVo {\n        log.info(\"[EntranceIssuedTicketUseCase][execute] QR 입장 처리 userId={} eventId={} ticketUuid={}\", userId, eventId, uuid)\n        return issuedTicketDomainService.processingEntranceIssuedTicket(eventId, uuid)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/issuedTicket/service/ReadIssuedTicketUseCase.kt",
    "content": "package band.gosrock.api.issuedTicket.service\n\nimport band.gosrock.api.issuedTicket.dto.response.RetrieveIssuedTicketDetailResponse\nimport band.gosrock.api.issuedTicket.mapper.IssuedTicketMapper\nimport band.gosrock.common.annotation.UseCase\n\n@UseCase\nclass ReadIssuedTicketUseCase(\n    private val issuedTicketMapper: IssuedTicketMapper,\n) {\n\n    /**\n     * 발급 티켓 상세 정보 API\n     *\n     * @param userId 현재 사용자 id\n     * @param uuid 발급 티켓 id\n     * @return RetrieveIssuedTicketDetailResponse\n     */\n    fun execute(userId: Long, uuid: String): RetrieveIssuedTicketDetailResponse {\n        return issuedTicketMapper.toIssuedTicketDetailResponse(userId, uuid)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/issuedTicket/service/ReadIssuedTicketsUseCase.kt",
    "content": "package band.gosrock.api.issuedTicket.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.GUEST\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.common.page.PageResponse\nimport band.gosrock.api.issuedTicket.dto.request.AdminIssuedTicketTableQueryRequest\nimport band.gosrock.api.issuedTicket.dto.response.IssuedTicketAdminTableElement\nimport band.gosrock.api.issuedTicket.mapper.IssuedTicketMapper\nimport band.gosrock.common.annotation.UseCase\nimport org.springframework.data.domain.Pageable\n\n@UseCase\nclass ReadIssuedTicketsUseCase(\n    private val issuedTicketMapper: IssuedTicketMapper,\n) {\n\n    /**\n     * 발급된 티켓 리스트 가져오기 API 일단 유즈케이스에 트랜잭션 걸어서 처리 IssuedTicket에 걸린 event와 user를 연관관계 매핑 없이 조회하려할 때\n     * 로직이 너무 복잡해짐 => 일단 연관관계 매핑 걸어두고 나중에 QueryDsl 설정 들어오면 바꿔야 할 듯 => QueryDsl 추가 완료\n     */\n    @HostRolesAllowed(role = GUEST, findHostFrom = EVENT_ID)\n    fun execute(\n        userId: Long,\n        pageable: Pageable,\n        eventId: Long,\n        queryRequest: AdminIssuedTicketTableQueryRequest,\n    ): PageResponse<IssuedTicketAdminTableElement> {\n        return issuedTicketMapper.toIssuedTicketAdminTableElementPageResponse(\n            pageable,\n            queryRequest.toCondition(eventId),\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/controller/OrderAdminController.kt",
    "content": "package band.gosrock.api.order.controller\n\nimport band.gosrock.api.common.page.PageResponse\nimport band.gosrock.api.order.docs.ApproveOrderExceptionDocs\nimport band.gosrock.api.order.docs.CancelOrderExceptionDocs\nimport band.gosrock.api.order.model.dto.request.AdminOrderTableQueryRequest\nimport band.gosrock.api.order.model.dto.response.OrderAdminTableElement\nimport band.gosrock.api.order.model.dto.response.OrderResponse\nimport band.gosrock.api.order.service.ApproveOrderUseCase\nimport band.gosrock.api.order.service.CancelOrderUseCase\nimport band.gosrock.api.order.service.ReadOrderUseCase\nimport band.gosrock.api.order.service.RefuseOrderUseCase\nimport band.gosrock.api.order.model.dto.request.CancelReasonRequest\nimport band.gosrock.common.annotation.ApiErrorExceptionsExample\nimport band.gosrock.common.annotation.CurrentUserId\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport jakarta.validation.Valid\nimport org.springdoc.core.annotations.ParameterObject\nimport org.springframework.data.domain.Pageable\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestBody\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RestController\n\n@SecurityRequirement(name = \"access-token\")\n@Tag(name = \"6-2. [이벤트관리] 주문관리 \")\n@RestController\n@RequestMapping(\"/api/v1/events/{eventId}/orders\")\nclass OrderAdminController(\n    private val approveOrderUseCase: ApproveOrderUseCase,\n    private val readOrderUseCase: ReadOrderUseCase,\n    private val cancelOrderUseCase: CancelOrderUseCase,\n    private val refuseOrderUseCase: RefuseOrderUseCase,\n) {\n    @Operation(summary = \"어드민 목록 내 테이블 조회 OrderStage 는 꼭 보내주삼!\")\n    @GetMapping\n    fun getEventOrders(\n        @CurrentUserId userId: Long,\n        @ParameterObject @Valid adminOrderTableQueryRequest: AdminOrderTableQueryRequest,\n        @ParameterObject pageable: Pageable,\n        @PathVariable eventId: Long,\n    ): PageResponse<OrderAdminTableElement> =\n        readOrderUseCase.getEventOrders(userId, eventId, adminOrderTableQueryRequest, pageable)\n\n    @Operation(summary = \"결제 취소요청. 호스트 관리자가 결제를 취소 시킵니다.! (호스트 관리자용(관리자쪽에서 사용))\")\n    @ApiErrorExceptionsExample(CancelOrderExceptionDocs::class)\n    @PostMapping(\"/{order_uuid}/cancel\")\n    fun cancelOrder(\n        @CurrentUserId userId: Long,\n        @PathVariable(\"eventId\") eventId: Long,\n        @PathVariable(\"order_uuid\") orderUuid: String,\n        @RequestBody(required = false) request: CancelReasonRequest?,\n    ): OrderResponse = cancelOrderUseCase.execute(userId, eventId, orderUuid, request?.reason)\n\n    @Operation(summary = \"주문 승인하기 . 호스트 관리자가 티켓 주문을 승인합니다.\")\n    @ApiErrorExceptionsExample(ApproveOrderExceptionDocs::class)\n    @PostMapping(\"/{order_uuid}/approve\")\n    fun confirmOrder(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @PathVariable(\"order_uuid\") orderUuid: String,\n    ): OrderResponse = approveOrderUseCase.execute(userId, eventId, orderUuid)\n\n    @Operation(summary = \"승인 주문 거절하기 . 호스트 관리자가 승인 대기중인 주문을 거절합니다.\")\n    @PostMapping(\"/{order_uuid}/refuse\")\n    fun refuseOrder(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @PathVariable(\"order_uuid\") orderUuid: String,\n        @RequestBody(required = false) request: CancelReasonRequest?,\n    ): OrderResponse = refuseOrderUseCase.execute(userId, eventId, orderUuid, request?.reason)\n\n    @Operation(summary = \"주문관리 리스트 페이지에서 주문 상세정보 조회할때\")\n    @GetMapping(\"/{order_uuid}\")\n    fun getEventOrderDetail(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @PathVariable(\"order_uuid\") orderUuid: String,\n    ): OrderResponse = readOrderUseCase.getEventOrderDetail(userId, eventId, orderUuid)\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/controller/OrderController.kt",
    "content": "package band.gosrock.api.order.controller\n\nimport band.gosrock.api.common.slice.SliceResponse\nimport band.gosrock.api.order.docs.ConfirmOrderExceptionDocs\nimport band.gosrock.api.order.docs.CreateOrderExceptionDocs\nimport band.gosrock.api.order.docs.FreeOrderExceptionDocs\nimport band.gosrock.api.order.docs.RefundOrderExceptionDocs\nimport band.gosrock.api.order.model.dto.request.CancelReasonRequest\nimport band.gosrock.api.order.model.dto.request.ConfirmOrderRequest\nimport band.gosrock.api.order.model.dto.request.CreateOrderRequest\nimport band.gosrock.api.order.model.dto.response.CreateOrderResponse\nimport band.gosrock.api.order.model.dto.response.OrderBriefElement\nimport band.gosrock.api.order.model.dto.response.OrderResponse\nimport band.gosrock.api.order.model.dto.response.OrderTicketResponse\nimport band.gosrock.api.order.service.ConfirmOrderUseCase\nimport band.gosrock.api.order.service.CreateOrderUseCase\nimport band.gosrock.api.order.service.CreateTossOrderUseCase\nimport band.gosrock.api.order.service.FreeOrderUseCase\nimport band.gosrock.api.order.service.ReadOrderUseCase\nimport band.gosrock.api.order.service.RefundOrderUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport band.gosrock.common.annotation.ApiErrorExceptionsExample\nimport band.gosrock.common.annotation.DevelopOnlyApi\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.response.PaymentsResponse\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport jakarta.validation.Valid\nimport org.springdoc.core.annotations.ParameterObject\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.web.PageableDefault\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestBody\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RequestParam\nimport org.springframework.web.bind.annotation.RestController\n\n@SecurityRequirement(name = \"access-token\")\n@Tag(name = \"6-1. [주문]\")\n@RestController\n@RequestMapping(\"/api/v1/orders\")\nclass OrderController(\n    private val createOrderUseCase: CreateOrderUseCase,\n    private val confirmOrderUseCase: ConfirmOrderUseCase,\n    private val freeOrderUseCase: FreeOrderUseCase,\n    private val refundOrderUseCase: RefundOrderUseCase,\n    private val readOrderUseCase: ReadOrderUseCase,\n    private val createTossOrderUseCase: CreateTossOrderUseCase,\n) {\n    @Operation(summary = \"토스페이먼츠에서 주문서를 생성합니다.(테스트용)\")\n    @DevelopOnlyApi\n    @PostMapping(\"/testToss/{order_uuid}\")\n    fun createTossOrderUseCase(@PathVariable(\"order_uuid\") orderUuid: String): PaymentsResponse =\n        createTossOrderUseCase.execute(orderUuid)\n\n    @Operation(summary = \"주문을 생성합니다. 장바구니 아이디를 주문서로 변환하는 작업을 합니다.\")\n    @ApiErrorExceptionsExample(CreateOrderExceptionDocs::class)\n    @PostMapping(\"/\")\n    fun createOrder(@CurrentUserId userId: Long, @RequestBody @Valid createOrderRequest: CreateOrderRequest): CreateOrderResponse =\n        createOrderUseCase.execute(userId, createOrderRequest)\n\n    @Operation(summary = \"결제 확인하기 . successUrl 로 돌아온 웹페이지에서 query 로 받은 응답값을 서버로 보냅니당.\")\n    @ApiErrorExceptionsExample(ConfirmOrderExceptionDocs::class)\n    @PostMapping(\"/{order_uuid}/confirm\")\n    fun confirmOrder(\n        @CurrentUserId userId: Long,\n        @PathVariable(\"order_uuid\") orderUuid: String,\n        @RequestBody confirmOrderRequest: ConfirmOrderRequest,\n    ): OrderResponse = confirmOrderUseCase.execute(userId, orderUuid, confirmOrderRequest)\n\n    @Operation(summary = \"주문을 무료로 결제합니다. 선착순 방식 결제 0원일 때 지원\")\n    @ApiErrorExceptionsExample(FreeOrderExceptionDocs::class)\n    @PostMapping(\"/{order_uuid}/free\")\n    fun freeOrder(@CurrentUserId userId: Long, @PathVariable(\"order_uuid\") orderUuid: String): OrderResponse =\n        freeOrderUseCase.execute(userId, orderUuid)\n\n    @Operation(summary = \"결제 환불요청. 본인이 구매한 오더를 환불 시킵니다.! (본인 용)\")\n    @ApiErrorExceptionsExample(RefundOrderExceptionDocs::class)\n    @PostMapping(\"/{order_uuid}/refund\")\n    fun refundOrder(\n        @CurrentUserId userId: Long,\n        @PathVariable(\"order_uuid\") orderUuid: String,\n        @RequestBody(required = false) request: CancelReasonRequest?,\n    ): OrderResponse =\n        refundOrderUseCase.execute(userId, orderUuid, request?.reason)\n\n    @Operation(summary = \"결제 조회. 결제 조회 권한은 주문 본인\")\n    @GetMapping(\"/{order_uuid}\")\n    fun getOrderDetail(@CurrentUserId userId: Long, @PathVariable(\"order_uuid\") orderUuid: String): OrderResponse =\n        readOrderUseCase.getOrderDetail(userId, orderUuid)\n\n    @Operation(summary = \"결제 아이디로 발급된 티켓 조회\")\n    @GetMapping(\"/{order_uuid}/tickets\")\n    fun getOrderTickets(@CurrentUserId userId: Long, @PathVariable(\"order_uuid\") orderUuid: String): OrderTicketResponse =\n        readOrderUseCase.getOrderTickets(userId, orderUuid)\n\n    @Operation(summary = \"최근 예매내역 조회\")\n    @GetMapping(\"/recent\")\n    fun getRecentOrder(@CurrentUserId userId: Long): OrderBriefElement? = readOrderUseCase.getRecentOrder(userId)\n\n    @Operation(summary = \"마이페이지 내 예매목록 조회\")\n    @GetMapping\n    fun getMyOrders(\n        @CurrentUserId userId: Long,\n        @ParameterObject @RequestParam showing: Boolean,\n        @ParameterObject @PageableDefault pageable: Pageable,\n    ): SliceResponse<OrderBriefElement> = readOrderUseCase.getMyOrders(userId, showing, pageable)\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/docs/ApproveOrderExceptionDocs.kt",
    "content": "package band.gosrock.api.order.docs\n\nimport band.gosrock.common.annotation.ExceptionDoc\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.exception.DuDoongCodeException\nimport band.gosrock.common.interfaces.SwaggerExampleExceptions\nimport band.gosrock.domain.domains.event.exception.EventNotOpenException\nimport band.gosrock.domain.domains.event.exception.EventTicketingTimeIsPassedException\nimport band.gosrock.domain.domains.order.exception.NotApprovalOrderException\nimport band.gosrock.domain.domains.order.exception.NotPendingOrderException\nimport band.gosrock.domain.domains.order.exception.OrdeItemNotOneTypeException\nimport band.gosrock.domain.domains.order.exception.OrderItemOptionChangedException\nimport band.gosrock.domain.domains.ticket_item.exception.TicketItemQuantityLackException\nimport band.gosrock.domain.domains.ticket_item.exception.TicketPurchaseLimitException\n\n@ExceptionDoc\nclass ApproveOrderExceptionDocs : SwaggerExampleExceptions {\n\n    @ExplainError(\"주문 방식이 승인 방식인지 검증합니다.\")\n    val 주문방식_승인방식_검증: DuDoongCodeException = NotApprovalOrderException.EXCEPTION\n\n    @ExplainError(\"주문상태가 대기중인 상태가 아닐 때\")\n    val 주문상태_검증: DuDoongCodeException = NotPendingOrderException.EXCEPTION\n\n    @ExplainError(\"이벤트가 열린 상태가 아닐때\")\n    val 이벤트_닫힘: DuDoongCodeException = EventNotOpenException.EXCEPTION\n\n    @ExplainError(\"이벤트 티켓팅 시간이 지났을때.\")\n    val 티켓팅_시간지남: DuDoongCodeException = EventTicketingTimeIsPassedException.EXCEPTION\n\n    @ExplainError(\"티켓 아이템이 한 종류가 아닐 떄\")\n    val 아이템은_한종류여야함: DuDoongCodeException = OrdeItemNotOneTypeException.EXCEPTION\n\n    @ExplainError(\"아이템의 재고가 부족한 상태일 때\")\n    val 티켓팅_재고부족: DuDoongCodeException = TicketItemQuantityLackException.EXCEPTION\n\n    @ExplainError(\"티켓당 1인당 구매갯수 제한을 넘었을때\")\n    val 티켓팅_구매갯수제한: DuDoongCodeException = TicketPurchaseLimitException.EXCEPTION\n\n    @ExplainError(\"주문 과정속에서 아이템의 옵션이 변화했을때 오류\")\n    val 아이템_옵션_변화: DuDoongCodeException = OrderItemOptionChangedException.EXCEPTION\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/docs/CancelOrderExceptionDocs.kt",
    "content": "package band.gosrock.api.order.docs\n\nimport band.gosrock.common.annotation.ExceptionDoc\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.exception.DuDoongCodeException\nimport band.gosrock.common.interfaces.SwaggerExampleExceptions\nimport band.gosrock.domain.domains.event.exception.EventNotOpenException\nimport band.gosrock.domain.domains.event.exception.EventTicketingTimeIsPassedException\nimport band.gosrock.domain.domains.order.exception.CanNotCancelOrderException\nimport band.gosrock.domain.domains.order.exception.NotRefundAvailableDateOrderException\n\n@ExceptionDoc\nclass CancelOrderExceptionDocs : SwaggerExampleExceptions {\n\n    @ExplainError(\"주문이 환불 가능한 시점인지 확인합니다.\")\n    val 주문_환불가능_시점확인: DuDoongCodeException = NotRefundAvailableDateOrderException.EXCEPTION\n\n    @ExplainError(\"주문상태가 취소가 가능한 상태여야 합니다.\")\n    val 주문상태_취소가능_검증: DuDoongCodeException = CanNotCancelOrderException.EXCEPTION\n\n    @ExplainError(\"이벤트가 열린 상태가 아닐때\")\n    val 이벤트_닫힘: DuDoongCodeException = EventNotOpenException.EXCEPTION\n\n    @ExplainError(\"이벤트 티켓팅 시간이 지났을때.\")\n    val 티켓팅_시간지남: DuDoongCodeException = EventTicketingTimeIsPassedException.EXCEPTION\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/docs/ConfirmOrderExceptionDocs.kt",
    "content": "package band.gosrock.api.order.docs\n\nimport band.gosrock.common.annotation.ExceptionDoc\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.exception.DuDoongCodeException\nimport band.gosrock.common.interfaces.SwaggerExampleExceptions\nimport band.gosrock.domain.domains.event.exception.EventNotOpenException\nimport band.gosrock.domain.domains.event.exception.EventTicketingTimeIsPassedException\nimport band.gosrock.domain.domains.order.exception.NotPaymentOrderException\nimport band.gosrock.domain.domains.order.exception.NotPendingOrderException\nimport band.gosrock.domain.domains.order.exception.OrdeItemNotOneTypeException\nimport band.gosrock.domain.domains.order.exception.OrderItemOptionChangedException\nimport band.gosrock.domain.domains.ticket_item.exception.TicketItemQuantityLackException\nimport band.gosrock.domain.domains.ticket_item.exception.TicketPurchaseLimitException\n\n@ExceptionDoc\nclass ConfirmOrderExceptionDocs : SwaggerExampleExceptions {\n\n    @ExplainError(\"주문 방식이 결제 방식인지 검증합니다.\")\n    val 주문방식_결제방식_검증: DuDoongCodeException = NotPaymentOrderException.EXCEPTION\n\n    @ExplainError(\"주문상태가 대기중인 상태가 아닐 때\")\n    val 주문상태_검증: DuDoongCodeException = NotPendingOrderException.EXCEPTION\n\n    @ExplainError(\"이벤트가 열린 상태가 아닐때\")\n    val 이벤트_닫힘: DuDoongCodeException = EventNotOpenException.EXCEPTION\n\n    @ExplainError(\"이벤트 티켓팅 시간이 지났을때.\")\n    val 티켓팅_시간지남: DuDoongCodeException = EventTicketingTimeIsPassedException.EXCEPTION\n\n    @ExplainError(\"티켓 아이템이 한 종류가 아닐 떄\")\n    val 아이템은_한종류여야함: DuDoongCodeException = OrdeItemNotOneTypeException.EXCEPTION\n\n    @ExplainError(\"아이템의 재고가 부족한 상태일 때\")\n    val 티켓팅_재고부족: DuDoongCodeException = TicketItemQuantityLackException.EXCEPTION\n\n    @ExplainError(\"티켓당 1인당 구매갯수 제한을 넘었을때\")\n    val 티켓팅_구매갯수제한: DuDoongCodeException = TicketPurchaseLimitException.EXCEPTION\n\n    @ExplainError(\"주문 과정속에서 아이템의 옵션이 변화했을때 오류\")\n    val 아이템_옵션_변화: DuDoongCodeException = OrderItemOptionChangedException.EXCEPTION\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/docs/CreateOrderExceptionDocs.kt",
    "content": "package band.gosrock.api.order.docs\n\nimport band.gosrock.common.annotation.ExceptionDoc\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.exception.DuDoongCodeException\nimport band.gosrock.common.interfaces.SwaggerExampleExceptions\nimport band.gosrock.domain.domains.event.exception.EventNotOpenException\nimport band.gosrock.domain.domains.event.exception.EventTicketingTimeIsPassedException\nimport band.gosrock.domain.domains.order.exception.InvalidOrderException\nimport band.gosrock.domain.domains.order.exception.OrdeItemNotOneTypeException\nimport band.gosrock.domain.domains.order.exception.OrderItemOptionChangedException\nimport band.gosrock.domain.domains.ticket_item.exception.TicketItemQuantityLackException\nimport band.gosrock.domain.domains.ticket_item.exception.TicketPurchaseLimitException\n\n@ExceptionDoc\nclass CreateOrderExceptionDocs : SwaggerExampleExceptions {\n\n    @ExplainError(\"선착순 주문이라 결제가 필요한 주문인데 승인으로 요청하거나 쿠폰이 적용된 주문인데 결제금액이 없는경우 등 잘못된 주문 요청\")\n    val 잘못된주문생성요청: DuDoongCodeException = InvalidOrderException.EXCEPTION\n\n    @ExplainError(\"이벤트가 열린 상태가 아닐때\")\n    val 이벤트_닫힘: DuDoongCodeException = EventNotOpenException.EXCEPTION\n\n    @ExplainError(\"이벤트 티켓팅 시간이 지났을때.\")\n    val 티켓팅_시간지남: DuDoongCodeException = EventTicketingTimeIsPassedException.EXCEPTION\n\n    @ExplainError(\"티켓 아이템이 한 종류가 아닐 떄\")\n    val 아이템은_한종류여야함: DuDoongCodeException = OrdeItemNotOneTypeException.EXCEPTION\n\n    @ExplainError(\"아이템의 재고가 부족한 상태일 때\")\n    val 티켓팅_재고부족: DuDoongCodeException = TicketItemQuantityLackException.EXCEPTION\n\n    @ExplainError(\"티켓당 1인당 구매갯수 제한을 넘었을때\")\n    val 티켓팅_구매갯수제한: DuDoongCodeException = TicketPurchaseLimitException.EXCEPTION\n\n    @ExplainError(\"주문 과정속에서 아이템의 옵션이 변화했을때 오류\")\n    val 아이템_옵션_변화: DuDoongCodeException = OrderItemOptionChangedException.EXCEPTION\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/docs/FreeOrderExceptionDocs.kt",
    "content": "package band.gosrock.api.order.docs\n\nimport band.gosrock.common.annotation.ExceptionDoc\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.exception.DuDoongCodeException\nimport band.gosrock.common.interfaces.SwaggerExampleExceptions\nimport band.gosrock.domain.domains.event.exception.EventNotOpenException\nimport band.gosrock.domain.domains.event.exception.EventTicketingTimeIsPassedException\nimport band.gosrock.domain.domains.order.exception.NotFreeOrderException\nimport band.gosrock.domain.domains.order.exception.NotPaymentOrderException\nimport band.gosrock.domain.domains.order.exception.NotPendingOrderException\nimport band.gosrock.domain.domains.order.exception.OrdeItemNotOneTypeException\nimport band.gosrock.domain.domains.order.exception.OrderItemOptionChangedException\nimport band.gosrock.domain.domains.ticket_item.exception.TicketItemQuantityLackException\nimport band.gosrock.domain.domains.ticket_item.exception.TicketPurchaseLimitException\n\n@ExceptionDoc\nclass FreeOrderExceptionDocs : SwaggerExampleExceptions {\n\n    @ExplainError(\"주문 금액이 0원인지 검증합니다.\")\n    val 주문_0원_검증: DuDoongCodeException = NotFreeOrderException.EXCEPTION\n\n    @ExplainError(\"주문 방식이 결제 방식인지 검증합니다.\")\n    val 주문방식_결제방식_검증: DuDoongCodeException = NotPaymentOrderException.EXCEPTION\n\n    @ExplainError(\"주문상태가 대기중인 상태가 아닐 때\")\n    val 주문상태_검증: DuDoongCodeException = NotPendingOrderException.EXCEPTION\n\n    @ExplainError(\"이벤트가 열린 상태가 아닐때\")\n    val 이벤트_닫힘: DuDoongCodeException = EventNotOpenException.EXCEPTION\n\n    @ExplainError(\"이벤트 티켓팅 시간이 지났을때.\")\n    val 티켓팅_시간지남: DuDoongCodeException = EventTicketingTimeIsPassedException.EXCEPTION\n\n    @ExplainError(\"티켓 아이템이 한 종류가 아닐 떄\")\n    val 아이템은_한종류여야함: DuDoongCodeException = OrdeItemNotOneTypeException.EXCEPTION\n\n    @ExplainError(\"아이템의 재고가 부족한 상태일 때\")\n    val 티켓팅_재고부족: DuDoongCodeException = TicketItemQuantityLackException.EXCEPTION\n\n    @ExplainError(\"티켓당 1인당 구매갯수 제한을 넘었을때\")\n    val 티켓팅_구매갯수제한: DuDoongCodeException = TicketPurchaseLimitException.EXCEPTION\n\n    @ExplainError(\"주문 과정속에서 아이템의 옵션이 변화했을때 오류\")\n    val 아이템_옵션_변화: DuDoongCodeException = OrderItemOptionChangedException.EXCEPTION\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/docs/RefundOrderExceptionDocs.kt",
    "content": "package band.gosrock.api.order.docs\n\nimport band.gosrock.common.annotation.ExceptionDoc\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.exception.DuDoongCodeException\nimport band.gosrock.common.interfaces.SwaggerExampleExceptions\nimport band.gosrock.domain.domains.event.exception.EventNotOpenException\nimport band.gosrock.domain.domains.event.exception.EventTicketingTimeIsPassedException\nimport band.gosrock.domain.domains.order.exception.CanNotRefundOrderException\nimport band.gosrock.domain.domains.order.exception.NotRefundAvailableDateOrderException\n\n@ExceptionDoc\nclass RefundOrderExceptionDocs : SwaggerExampleExceptions {\n\n    @ExplainError(\"주문이 환불 가능한 시점인지 확인합니다.\")\n    val 주문_환불가능_시점확인: DuDoongCodeException = NotRefundAvailableDateOrderException.EXCEPTION\n\n    @ExplainError(\"주문상태가 취소가 가능한 상태여야 합니다.\")\n    val 주문상태_취소가능_검증: DuDoongCodeException = CanNotRefundOrderException.EXCEPTION\n\n    @ExplainError(\"이벤트가 열린 상태가 아닐때\")\n    val 이벤트_닫힘: DuDoongCodeException = EventNotOpenException.EXCEPTION\n\n    @ExplainError(\"이벤트 티켓팅 시간이 지났을때.\")\n    val 티켓팅_시간지남: DuDoongCodeException = EventTicketingTimeIsPassedException.EXCEPTION\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/model/dto/request/AdminOrderTableQueryRequest.kt",
    "content": "package band.gosrock.api.order.model.dto.request\n\nimport band.gosrock.common.annotation.Enum\nimport band.gosrock.domain.domains.order.repository.condition.AdminTableOrderFilterType\nimport band.gosrock.domain.domains.order.repository.condition.AdminTableSearchType\nimport band.gosrock.domain.domains.order.repository.condition.FindEventOrdersCondition\nimport com.fasterxml.jackson.annotation.JsonIgnore\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class AdminOrderTableQueryRequest(\n    @field:Enum\n    val orderStage: AdminTableOrderFilterType,\n\n    // nullable\n    val searchType: AdminTableSearchType?,\n\n    @Schema(nullable = true)\n    val searchString: String?,\n) {\n    @JsonIgnore\n    fun toCondition(eventId: Long): FindEventOrdersCondition =\n        FindEventOrdersCondition(\n            eventId = eventId,\n            filterType = orderStage,\n            searchString = searchString,\n            searchType = searchType,\n        )\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/model/dto/request/CancelReasonRequest.kt",
    "content": "package band.gosrock.api.order.model.dto.request\n\ndata class CancelReasonRequest(val reason: String? = null)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/model/dto/request/ConfirmOrderRequest.kt",
    "content": "package band.gosrock.api.order.model.dto.request\n\ndata class ConfirmOrderRequest(\n    val paymentKey: String?,\n    val amount: Long?,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/model/dto/request/CreateOrderRequest.kt",
    "content": "package band.gosrock.api.order.model.dto.request\n\nimport io.swagger.v3.oas.annotations.media.Schema\nimport jakarta.validation.constraints.NotNull\n\ndata class CreateOrderRequest(\n    @Schema(nullable = true, defaultValue = \"null\")\n    val couponId: Long?,\n\n    @field:NotNull\n    val cartId: Long?,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/model/dto/response/CreateOrderResponse.kt",
    "content": "package band.gosrock.api.order.model.dto.response\n\nimport band.gosrock.domain.common.vo.AccountInfoVo\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderMethod\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport band.gosrock.domain.domains.ticket_item.domain.TicketPayType\nimport band.gosrock.domain.domains.ticket_item.domain.TicketType\nimport band.gosrock.domain.domains.user.domain.Profile\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class CreateOrderResponse(\n    @Schema(description = \"UUId\")\n    val orderId: String,\n\n    @Schema(description = \"상품명\")\n    val orderName: String,\n\n    @Schema(description = \"고객이메일\")\n    val customerEmail: String,\n\n    @Schema(description = \"고객이름\")\n    val customerName: String,\n\n    @Schema(description = \"결제금액\")\n    val amount: Money,\n\n    @Schema(description = \"결제가 필요한지에 대한 여부를 결정합니다. 필요한 true면 결제창 띄우시면됩니다.\", defaultValue = \"true\")\n    val isNeedPayment: Boolean,\n\n    @Schema(description = \"주문 방식 ( 결제 방식 , 승인 방식 )\")\n    val orderMethod: OrderMethod,\n\n    @Schema(description = \"티켓의 타입. 승인 , 선착순 두가지입니다.\")\n    val approveType: TicketType,\n\n    @Schema(description = \"티켓의 지불 타입. 두둥티켓, 무료 , 유료 세가지입니다.\")\n    val ticketPayType: TicketPayType,\n\n    @Schema(description = \"계좌정보\", nullable = true)\n    val accountInfo: AccountInfoVo?,\n) {\n    companion object {\n        @JvmStatic\n        fun from(order: Order, item: TicketItem, profile: Profile): CreateOrderResponse =\n            CreateOrderResponse(\n                customerEmail = profile.email!!,\n                customerName = profile.name!!,\n                orderName = order.orderName!!,\n                orderId = order.uuid!!,\n                amount = order.getTotalPaymentPrice(),\n                orderMethod = order.orderMethod!!,\n                isNeedPayment = order.isNeedPaid(),\n                approveType = item.type!!,\n                ticketPayType = item.payType!!,\n                accountInfo = item.accountInfo,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/model/dto/response/OrderAdminTableElement.kt",
    "content": "package band.gosrock.api.order.model.dto.response\n\nimport band.gosrock.common.annotation.DateFormat\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.common.vo.RefundInfoVo\nimport band.gosrock.domain.common.vo.UserInfoVo\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport band.gosrock.domain.domains.user.domain.User\nimport io.swagger.v3.oas.annotations.media.Schema\nimport java.time.LocalDateTime\n\ndata class OrderAdminTableElement(\n    @Schema(description = \"취소 가능 정보\")\n    val refundInfo: RefundInfoVo,\n\n    @Schema(description = \"유저 정보\")\n    val userInfoVo: UserInfoVo,\n\n    @Schema(description = \"주문 고유 uuid 대체키임\")\n    val orderUuid: String,\n\n    @Schema(description = \"주문 번호 R------- 형식\")\n    val orderNo: String,\n\n    @Schema(description = \"주문의 상태\")\n    val orderStatus: OrderStatus,\n\n    @Schema(description = \"주문 이름\")\n    val orderName: String,\n\n    @Schema(description = \"주문 생성 시간\")\n    @DateFormat\n    val createdAt: LocalDateTime,\n\n    @Schema(description = \"철회 완료 시간\")\n    @DateFormat\n    val withDrawAt: LocalDateTime?,\n\n    @Schema(description = \"승인 된 시간\")\n    @DateFormat\n    val approveAt: LocalDateTime?,\n\n    @Schema(description = \"아이템 총 갯수\")\n    val totalQuantity: Long,\n\n    @Schema(description = \"아이템 총 갯수\")\n    val totalPaymentPrice: Money,\n) {\n    companion object {\n        @JvmStatic\n        fun of(order: Order, event: Event, user: User): OrderAdminTableElement =\n            OrderAdminTableElement(\n                refundInfo = event.toRefundInfoVoWithOrderStatus(order.orderStatus),\n                orderUuid = order.uuid!!,\n                orderNo = order.orderNo!!,\n                orderStatus = order.orderStatus,\n                userInfoVo = user.toUserInfoVo(),\n                orderName = order.orderName!!,\n                createdAt = order.createdAt!!,\n                withDrawAt = order.withDrawAt,\n                approveAt = order.approvedAt,\n                totalQuantity = order.getTotalQuantity(),\n                totalPaymentPrice = order.getTotalPaymentPrice(),\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/model/dto/response/OrderBriefElement.kt",
    "content": "package band.gosrock.api.order.model.dto.response\n\nimport band.gosrock.domain.common.vo.EventProfileVo\nimport band.gosrock.domain.common.vo.RefundInfoVo\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTickets\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketsStage\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class OrderBriefElement(\n    @Schema(description = \"예매 취소 정보\")\n    val refundInfo: RefundInfoVo,\n\n    @Schema(description = \"이벤트 프로필 정보\")\n    val eventProfile: EventProfileVo,\n\n    @Schema(description = \"주문 고유 uuid\")\n    val orderUuid: String,\n\n    @Schema(description = \"주문 번호 R------- 형식\")\n    val orderNo: String,\n\n    @Schema(description = \"주문에 딸린 티켓들의 상태\")\n    val stage: IssuedTicketsStage,\n\n    @Schema(description = \"주문의 상태\")\n    val orderStatus: OrderStatus,\n\n    @Schema(description = \"아이템 이름\")\n    val itemName: String,\n\n    @Schema(description = \"아이템 총 갯수\")\n    val totalQuantity: Long,\n) {\n    companion object {\n        @JvmStatic\n        fun of(order: Order, event: Event, issuedTickets: IssuedTickets): OrderBriefElement =\n            OrderBriefElement(\n                refundInfo = event.toRefundInfoVoWithOrderStatus(order.orderStatus),\n                stage = issuedTickets.getIssuedTicketsStage(event),\n                orderUuid = order.uuid!!,\n                orderNo = order.orderNo!!,\n                orderStatus = order.orderStatus,\n                eventProfile = event.toEventProfileVo(),\n                itemName = order.orderName!!,\n                totalQuantity = order.getTotalQuantity(),\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/model/dto/response/OrderLineTicketResponse.kt",
    "content": "package band.gosrock.api.order.model.dto.response\n\nimport band.gosrock.common.annotation.DateFormat\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.common.vo.OptionAnswerVo\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderLineItem\nimport io.swagger.v3.oas.annotations.media.Schema\nimport java.time.LocalDateTime\n\ndata class OrderLineTicketResponse(\n    @Schema(description = \"티켓명\", defaultValue = \"일반티켓\")\n    val ticketName: String,\n\n    @Schema(description = \"예매 번호\", defaultValue = \"R1000001-123\")\n    val orderNo: String,\n\n    @Schema(description = \"티켓 번호\", defaultValue = \"T1000001 ~ T1000002 (2매)\")\n    val ticketNos: String,\n\n    @Schema(description = \"구매 일시\")\n    @DateFormat\n    val paymentAt: LocalDateTime?,\n\n    @Schema(description = \"유저이름\")\n    val userName: String,\n\n    @Schema(description = \"금액\")\n    val orderLinePrice: Money,\n\n    @Schema(description = \"구매수량\")\n    val purchaseQuantity: Long,\n\n    @Schema(description = \"옵션의 응답 목록\")\n    val answers: List<OptionAnswerVo>,\n\n    @Schema(description = \"각 옵션 가격\")\n    val eachOptionPrice: Money,\n) {\n    companion object {\n        @JvmStatic\n        fun of(\n            order: Order,\n            orderLineItem: OrderLineItem,\n            optionAnswerVos: List<OptionAnswerVo>,\n            userName: String,\n            ticketNos: String,\n        ): OrderLineTicketResponse =\n            OrderLineTicketResponse(\n                answers = optionAnswerVos,\n                orderNo = order.orderNo + \"-\" + orderLineItem.id,\n                ticketNos = ticketNos,\n                ticketName = orderLineItem.getItemName(),\n                paymentAt = order.approvedAt,\n                userName = userName,\n                orderLinePrice = orderLineItem.getTotalOrderLinePrice(),\n                purchaseQuantity = orderLineItem.quantity!!,\n                eachOptionPrice = orderLineItem.getOptionAnswersPrice(),\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/model/dto/response/OrderPaymentResponse.kt",
    "content": "package band.gosrock.api.order.model.dto.response\n\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class OrderPaymentResponse(\n    @Schema(description = \"결제 수단 ( 승인 결제 , 간편 결제 , 카드 결제 등 )\", defaultValue = \"간편결제\")\n    val paymentMethod: String?,\n\n    @Schema(\n        description = \"공급자 ( 카카오페이 , 현대카드 등 승인결제일경우 null 입니다. )\",\n        defaultValue = \"카카오페이\",\n        nullable = true,\n    )\n    val provider: String?,\n\n    @Schema(description = \"공급가액 (금액)\", defaultValue = \"12000원\")\n    val supplyAmount: Money,\n\n    @Schema(description = \"할인 금액\", defaultValue = \"1000원\")\n    val discountAmount: Money,\n\n    @Schema(description = \"할인쿠폰 이름\", defaultValue = \"사용하지 않음\")\n    val couponName: String?,\n\n    @Schema(description = \"총 결제 금액\", defaultValue = \"11000원\")\n    val totalAmount: Money,\n\n    @Schema(description = \"결제 상태\", defaultValue = \"결제 완료\")\n    val orderStatus: OrderStatus,\n\n    @Schema(description = \"영수증 주소\", defaultValue = \"영수증주소\", nullable = true)\n    val receiptUrl: String?,\n) {\n    companion object {\n        @JvmStatic\n        fun from(order: Order): OrderPaymentResponse {\n            val totalPaymentInfo = order.totalPaymentInfo\n            return OrderPaymentResponse(\n                paymentMethod = order.getMethod(),\n                provider = order.getProvider(),\n                discountAmount = totalPaymentInfo!!.discountAmount!!,\n                supplyAmount = totalPaymentInfo.supplyAmount!!,\n                totalAmount = totalPaymentInfo.paymentAmount!!,\n                couponName = order.getCouponName(),\n                orderStatus = order.orderStatus,\n                receiptUrl = order.getReceiptUrl(),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/model/dto/response/OrderResponse.kt",
    "content": "package band.gosrock.api.order.model.dto.response\n\nimport band.gosrock.domain.common.vo.EventProfileVo\nimport band.gosrock.domain.common.vo.RefundInfoVo\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderMethod\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class OrderResponse(\n    @Schema(description = \"결제 정보\")\n    val paymentInfo: OrderPaymentResponse,\n\n    @Schema(description = \"예매 정보( 티켓 목록 )\")\n    val tickets: List<OrderLineTicketResponse>,\n\n    @Schema(description = \"예매 취소 정보\")\n    val refundInfo: RefundInfoVo,\n\n    @Schema(description = \"이벤트 프로필 정보\")\n    val eventProfile: EventProfileVo,\n\n    @Schema(description = \"주문 고유 uuid\")\n    val orderUuid: String,\n\n    @Schema(description = \"주문 번호 R------- 형식\")\n    val orderNo: String,\n\n    @Schema(description = \"주문 방식 ( 결제 방식 , 승인 방식 )\")\n    val orderMethod: OrderMethod,\n) {\n    companion object {\n        @JvmStatic\n        fun of(order: Order, event: Event, tickets: List<OrderLineTicketResponse>): OrderResponse =\n            OrderResponse(\n                refundInfo = event.toRefundInfoVoWithOrderStatus(order.orderStatus),\n                orderMethod = order.orderMethod!!,\n                paymentInfo = OrderPaymentResponse.from(order),\n                tickets = tickets,\n                orderUuid = order.uuid!!,\n                orderNo = order.orderNo!!,\n                eventProfile = event.toEventProfileVo(),\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/model/dto/response/OrderTicketResponse.kt",
    "content": "package band.gosrock.api.order.model.dto.response\n\nimport band.gosrock.domain.common.vo.EventProfileVo\nimport band.gosrock.domain.common.vo.IssuedTicketInfoVo\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.order.domain.Order\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class OrderTicketResponse(\n    @Schema(description = \"예매 정보( 티켓 목록 )\")\n    val tickets: List<IssuedTicketInfoVo>,\n\n    @Schema(description = \"이벤트 프로필 정보\")\n    val eventProfile: EventProfileVo,\n\n    @Schema(description = \"주문 고유 uuid\")\n    val orderUuid: String,\n\n    @Schema(description = \"주문 번호 R------- 형식\")\n    val orderNo: String,\n) {\n    companion object {\n        @JvmStatic\n        fun of(order: Order, event: Event, tickets: List<IssuedTicketInfoVo>): OrderTicketResponse =\n            OrderTicketResponse(\n                tickets = tickets,\n                orderUuid = order.uuid!!,\n                orderNo = order.orderNo!!,\n                eventProfile = event.toEventProfileVo(),\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/model/mapper/OrderMapper.kt",
    "content": "package band.gosrock.api.order.model.mapper\n\nimport band.gosrock.api.order.model.dto.response.CreateOrderResponse\nimport band.gosrock.api.order.model.dto.response.OrderAdminTableElement\nimport band.gosrock.api.order.model.dto.response.OrderBriefElement\nimport band.gosrock.api.order.model.dto.response.OrderLineTicketResponse\nimport band.gosrock.api.order.model.dto.response.OrderResponse\nimport band.gosrock.api.order.model.dto.response.OrderTicketResponse\nimport band.gosrock.common.annotation.Mapper\nimport band.gosrock.domain.common.vo.IssuedTicketInfoVo\nimport band.gosrock.domain.common.vo.OptionAnswerVo\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.issuedTicket.adaptor.IssuedTicketAdaptor\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTickets\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderLineItem\nimport band.gosrock.domain.domains.order.domain.OrderOptionAnswer\nimport band.gosrock.domain.domains.ticket_item.adaptor.OptionAdaptor\nimport band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor\nimport band.gosrock.domain.domains.ticket_item.domain.Option\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.domain.domains.user.domain.User\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Slice\nimport org.springframework.transaction.annotation.Transactional\n\n@Mapper\nclass OrderMapper(\n    private val orderAdaptor: OrderAdaptor,\n    private val userAdaptor: UserAdaptor,\n    private val issuedTicketAdaptor: IssuedTicketAdaptor,\n    private val ticketItemAdaptor: TicketItemAdaptor,\n    private val optionAdaptor: OptionAdaptor,\n    private val eventAdaptor: EventAdaptor,\n) {\n    @Transactional(readOnly = true)\n    fun toOrderResponse(orderUuid: String): OrderResponse {\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        val event = getEvent(order)\n        val orderLineTicketResponses = getOrderLineTicketResponses(order)\n        return OrderResponse.of(order, event, orderLineTicketResponses)\n    }\n\n    @Transactional(readOnly = true)\n    fun toOrderResponse(order: Order): OrderResponse {\n        val event = getEvent(order)\n        val orderLineTicketResponses = getOrderLineTicketResponses(order)\n        return OrderResponse.of(order, event, orderLineTicketResponses)\n    }\n\n    @Transactional(readOnly = true)\n    fun toCreateOrderResponse(orderUuid: String): CreateOrderResponse {\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        val user = userAdaptor.queryUser(order.userId)\n        val item = ticketItemAdaptor.queryTicketItem(order.itemId)\n        return CreateOrderResponse.from(order, item, user.profile!!)\n    }\n\n    private fun getOrderLineTicketResponses(order: Order): List<OrderLineTicketResponse> {\n        val user = userAdaptor.queryUser(order.userId)\n        return order.orderLineItems.map { orderLineItem ->\n            OrderLineTicketResponse.of(\n                order,\n                orderLineItem,\n                getOptionAnswerVos(orderLineItem),\n                user.profile!!.name!!,\n                getTicketNoName(orderLineItem.id!!),\n            )\n        }\n    }\n\n    private fun getOptionAnswerVos(orderLineItem: OrderLineItem): List<OptionAnswerVo> {\n        // TODO: options 일급 컬렉션으로 리팩터링\n        val options = optionAdaptor.findAllByIds(orderLineItem.getAnswerOptionIds())\n        return orderLineItem.orderOptionAnswers.map { orderOptionAnswer ->\n            orderOptionAnswer.getOptionAnswerVo(getOption(options, orderOptionAnswer))\n        }\n    }\n\n    private fun getOption(options: List<Option>, orderOptionAnswer: OrderOptionAnswer): Option =\n        options.first { it.id == orderOptionAnswer.optionId }\n\n    private fun getTicketNoName(orderLineItemId: Long): String =\n        issuedTicketAdaptor.findOrderLineIssuedTickets(orderLineItemId).getTicketNoName()\n\n    fun toOrderBriefElement(order: Order): OrderBriefElement {\n        val orderIssuedTickets = issuedTicketAdaptor.findOrderIssuedTickets(order.uuid!!)\n        return OrderBriefElement.of(order, getEvent(order), orderIssuedTickets)\n    }\n\n    fun toOrderBriefsResponse(ordersWithPagination: Slice<Order>): Slice<OrderBriefElement> =\n        ordersWithPagination.map { toOrderBriefElement(it) }\n\n    fun toOrderAdminTableElement(eventId: Long, orders: Page<Order>): Page<OrderAdminTableElement> {\n        val userIds = orders.map { it.userId!! }.distinct().toList()\n        val users = userAdaptor.findUserByIdIn(userIds)\n        val event = eventAdaptor.findById(eventId)\n        return orders.map { order ->\n            val user = users.first { it.id == order.userId }\n            OrderAdminTableElement.of(order, event, user)\n        }\n    }\n\n    private fun getEvent(order: Order): Event = eventAdaptor.findById(order.getItemGroupId())\n\n    fun toOrderTicketResponse(order: Order): OrderTicketResponse {\n        val orderIssuedTickets = issuedTicketAdaptor.findOrderIssuedTickets(order.uuid!!)\n        val event = getEvent(order)\n        val issuedTicketInfoVos: List<IssuedTicketInfoVo> = orderIssuedTickets.getIssuedTicketInfoVos()\n        return OrderTicketResponse.of(order, event, issuedTicketInfoVos)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/service/ApproveOrderUseCase.kt",
    "content": "package band.gosrock.api.order.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.MANAGER\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.order.model.dto.response.OrderResponse\nimport band.gosrock.api.order.model.mapper.OrderMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.order.service.OrderApproveService\nimport org.slf4j.LoggerFactory\n\n@UseCase\nclass ApproveOrderUseCase(\n    private val orderApproveService: OrderApproveService,\n    private val orderMapper: OrderMapper,\n) {\n    private val log = LoggerFactory.getLogger(ApproveOrderUseCase::class.java)\n\n    @HostRolesAllowed(role = MANAGER, findHostFrom = EVENT_ID, applyTransaction = false)\n    fun execute(userId: Long, eventId: Long, orderUuid: String): OrderResponse {\n        log.info(\"[ApproveOrderUseCase][execute] 주문 승인 userId={} eventId={} orderUuid={}\", userId, eventId, orderUuid)\n        val confirmOrderUuid = orderApproveService.execute(orderUuid)\n        return orderMapper.toOrderResponse(confirmOrderUuid)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/service/CancelOrderUseCase.kt",
    "content": "package band.gosrock.api.order.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.MANAGER\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.order.model.dto.response.OrderResponse\nimport band.gosrock.api.order.model.mapper.OrderMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.order.service.WithdrawOrderService\nimport org.slf4j.LoggerFactory\n\n@UseCase\nclass CancelOrderUseCase(\n    private val withdrawOrderService: WithdrawOrderService,\n    private val orderMapper: OrderMapper,\n) {\n    private val log = LoggerFactory.getLogger(CancelOrderUseCase::class.java)\n\n    @HostRolesAllowed(role = MANAGER, findHostFrom = EVENT_ID, applyTransaction = false)\n    fun execute(userId: Long, eventId: Long, orderUuid: String, reason: String? = null): OrderResponse {\n        log.info(\"[CancelOrderUseCase][execute] 주문 취소 userId={} eventId={} orderUuid={} reason={}\", userId, eventId, orderUuid, reason)\n        withdrawOrderService.cancelOrder(orderUuid, reason)\n        return orderMapper.toOrderResponse(orderUuid)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/service/ConfirmOrderUseCase.kt",
    "content": "package band.gosrock.api.order.service\n\nimport band.gosrock.api.order.model.dto.request.ConfirmOrderRequest\nimport band.gosrock.api.order.model.dto.response.OrderResponse\nimport band.gosrock.api.order.model.mapper.OrderMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.order.service.OrderConfirmService\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.request.ConfirmPaymentsRequest\nimport org.slf4j.LoggerFactory\n\n@UseCase\nclass ConfirmOrderUseCase(\n    private val orderConfirmService: OrderConfirmService,\n    private val orderMapper: OrderMapper,\n) {\n    private val log = LoggerFactory.getLogger(ConfirmOrderUseCase::class.java)\n\n    fun execute(userId: Long, orderUuid: String, confirmOrderRequest: ConfirmOrderRequest): OrderResponse {\n        log.info(\"[ConfirmOrderUseCase][execute] 결제 확인 userId={} orderUuid={} amount={}\", userId, orderUuid, confirmOrderRequest.amount)\n        val confirmPaymentsRequest = ConfirmPaymentsRequest(\n            paymentKey = confirmOrderRequest.paymentKey,\n            amount = confirmOrderRequest.amount,\n            orderId = orderUuid,\n        )\n        val confirmOrderUuid = orderConfirmService.execute(confirmPaymentsRequest, userId)\n        return orderMapper.toOrderResponse(confirmOrderUuid)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/service/CreateOrderUseCase.kt",
    "content": "package band.gosrock.api.order.service\n\nimport band.gosrock.api.order.model.dto.request.CreateOrderRequest\nimport band.gosrock.api.order.model.dto.response.CreateOrderResponse\nimport band.gosrock.api.order.model.mapper.OrderMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.order.service.CreateOrderService\nimport org.slf4j.LoggerFactory\n\n@UseCase\nclass CreateOrderUseCase(\n    private val createOrderService: CreateOrderService,\n    private val orderMapper: OrderMapper,\n) {\n    private val log = LoggerFactory.getLogger(CreateOrderUseCase::class.java)\n\n    fun execute(userId: Long, createOrderRequest: CreateOrderRequest): CreateOrderResponse {\n        log.info(\"[CreateOrderUseCase][execute] 주문 생성 userId={} cartId={} couponId={}\", userId, createOrderRequest.cartId, createOrderRequest.couponId)\n        val couponId = createOrderRequest.couponId\n        val cartId = createOrderRequest.cartId!!\n        return if (couponId == null) {\n            orderMapper.toCreateOrderResponse(createOrderService.withOutCoupon(cartId, userId))\n        } else {\n            orderMapper.toCreateOrderResponse(createOrderService.withCoupon(cartId, userId, couponId))\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/service/CreateTossOrderUseCase.kt",
    "content": "package band.gosrock.api.order.service\n\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.infrastructure.outer.api.tossPayments.client.PaymentsCreateClient\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.request.CreatePaymentsRequest\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.response.PaymentsResponse\nimport org.slf4j.LoggerFactory\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass CreateTossOrderUseCase(\n    private val paymentsCreateClient: PaymentsCreateClient,\n    private val orderAdaptor: OrderAdaptor,\n) {\n    private val log = LoggerFactory.getLogger(CreateTossOrderUseCase::class.java)\n\n    fun execute(orderUuid: String): PaymentsResponse {\n        log.info(\"[CreateTossOrderUseCase][execute] Toss 결제 생성 orderUuid={}\", orderUuid)\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        val createPaymentsRequest = CreatePaymentsRequest(\n            method = \"카드\",\n            orderName = order.orderName,\n            orderId = orderUuid,\n            failUrl = \"http://localhost:8080/failurl\",\n            successUrl = \"http://localhost:8080/successUrl\",\n            amount = order.getTotalPaymentPrice().longValue(),\n        )\n        return paymentsCreateClient.execute(createPaymentsRequest)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/service/FreeOrderUseCase.kt",
    "content": "package band.gosrock.api.order.service\n\nimport band.gosrock.api.order.model.dto.response.OrderResponse\nimport band.gosrock.api.order.model.mapper.OrderMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.order.service.FreeOrderService\nimport org.slf4j.LoggerFactory\n\n@UseCase\nclass FreeOrderUseCase(\n    private val freeOrderService: FreeOrderService,\n    private val orderMapper: OrderMapper,\n) {\n    private val log = LoggerFactory.getLogger(FreeOrderUseCase::class.java)\n\n    fun execute(userId: Long, orderUuid: String): OrderResponse {\n        log.info(\"[FreeOrderUseCase][execute] 무료 주문 확정 userId={} orderUuid={}\", userId, orderUuid)\n        val confirmOrderUuid = freeOrderService.execute(orderUuid, userId)\n        return orderMapper.toOrderResponse(confirmOrderUuid)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/service/ReadOrderUseCase.kt",
    "content": "package band.gosrock.api.order.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.GUEST\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.common.page.PageResponse\nimport band.gosrock.api.common.slice.SliceResponse\nimport band.gosrock.api.order.model.dto.request.AdminOrderTableQueryRequest\nimport band.gosrock.api.order.model.dto.response.OrderAdminTableElement\nimport band.gosrock.api.order.model.dto.response.OrderBriefElement\nimport band.gosrock.api.order.model.dto.response.OrderResponse\nimport band.gosrock.api.order.model.dto.response.OrderTicketResponse\nimport band.gosrock.api.order.model.mapper.OrderMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.issuedTicket.adaptor.IssuedTicketAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.validator.OrderValidator\nimport band.gosrock.domain.domains.order.repository.condition.FindMyPageOrderCondition\nimport org.springframework.data.domain.Pageable\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass ReadOrderUseCase(\n    private val orderMapper: OrderMapper,\n    private val orderAdaptor: OrderAdaptor,\n    private val orderValidator: OrderValidator,\n    private val issuedTicketAdaptor: IssuedTicketAdaptor,\n) {\n    fun getOrderDetail(userId: Long, orderUuid: String): OrderResponse {\n        val order = getMyOrder(userId, orderUuid)\n        return orderMapper.toOrderResponse(order)\n    }\n\n    private fun getMyOrder(userId: Long, orderUuid: String): Order {\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        orderValidator.validOwner(order, userId)\n        return order\n    }\n\n    fun getRecentOrder(userId: Long): OrderBriefElement? {\n        val recentOrder = orderAdaptor.findRecentOrderByUserId(userId)\n        return recentOrder.map { orderMapper.toOrderBriefElement(it) }.orElse(null)\n    }\n\n    fun getMyOrders(userId: Long, showing: Boolean, pageable: Pageable): SliceResponse<OrderBriefElement> {\n        val condition = if (showing == true) {\n            FindMyPageOrderCondition.onShowing(userId)\n        } else {\n            FindMyPageOrderCondition.notShowing(userId)\n        }\n        val ordersWithPagination = orderAdaptor.findMyOrders(condition, pageable)\n        val orderBriefElements = orderMapper.toOrderBriefsResponse(ordersWithPagination)\n        return SliceResponse.of(orderBriefElements)\n    }\n\n    @HostRolesAllowed(role = GUEST, findHostFrom = EVENT_ID)\n    fun getEventOrders(\n        userId: Long,\n        eventId: Long,\n        adminOrderTableQueryRequest: AdminOrderTableQueryRequest,\n        pageable: Pageable,\n    ): PageResponse<OrderAdminTableElement> {\n        val orders = orderAdaptor.findEventOrders(adminOrderTableQueryRequest.toCondition(eventId), pageable)\n        return PageResponse.of(orderMapper.toOrderAdminTableElement(eventId, orders))\n    }\n\n    @HostRolesAllowed(role = GUEST, findHostFrom = EVENT_ID)\n    fun getEventOrderDetail(userId: Long, eventId: Long, orderUuid: String): OrderResponse {\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        return orderMapper.toOrderResponse(order)\n    }\n\n    fun getOrderTickets(userId: Long, orderUuid: String): OrderTicketResponse {\n        val order = getMyOrder(userId, orderUuid)\n        return orderMapper.toOrderTicketResponse(order)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/service/RefundOrderUseCase.kt",
    "content": "package band.gosrock.api.order.service\n\nimport band.gosrock.api.order.model.dto.response.OrderResponse\nimport band.gosrock.api.order.model.mapper.OrderMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.order.service.WithdrawOrderService\nimport org.slf4j.LoggerFactory\n\n/** 환불을 위함 환불이라 함은, 사용자가 직접 구매한 물품을 취소시킴 */\n@UseCase\nclass RefundOrderUseCase(\n    private val withdrawOrderService: WithdrawOrderService,\n    private val orderMapper: OrderMapper,\n) {\n\n    private val log = LoggerFactory.getLogger(RefundOrderUseCase::class.java)\n\n    fun execute(userId: Long, orderUuid: String, reason: String? = null): OrderResponse {\n        log.info(\"[RefundOrderUseCase][execute] 환불 요청 userId={} orderUuid={} reason={}\", userId, orderUuid, reason)\n        withdrawOrderService.refundOrder(orderUuid, userId, reason)\n        return orderMapper.toOrderResponse(orderUuid)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/order/service/RefuseOrderUseCase.kt",
    "content": "package band.gosrock.api.order.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.MANAGER\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.order.model.dto.response.OrderResponse\nimport band.gosrock.api.order.model.mapper.OrderMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.order.service.WithdrawOrderService\nimport org.slf4j.LoggerFactory\n\n@UseCase\nclass RefuseOrderUseCase(\n    private val withdrawOrderService: WithdrawOrderService,\n    private val orderMapper: OrderMapper,\n) {\n    private val log = LoggerFactory.getLogger(RefuseOrderUseCase::class.java)\n\n    @HostRolesAllowed(role = MANAGER, findHostFrom = EVENT_ID, applyTransaction = false)\n    fun execute(userId: Long, eventId: Long, orderUuid: String, reason: String? = null): OrderResponse {\n        log.info(\"[RefuseOrderUseCase][execute] 주문 거부 userId={} eventId={} orderUuid={} reason={}\", userId, eventId, orderUuid, reason)\n        withdrawOrderService.refuseOrder(orderUuid, reason)\n        return orderMapper.toOrderResponse(orderUuid)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/refund/controller/RefundController.kt",
    "content": "package band.gosrock.api.refund.controller\n\nimport band.gosrock.api.common.page.PageResponse\nimport band.gosrock.api.refund.dto.response.RefundResponse\nimport band.gosrock.api.refund.service.CompleteRefundUseCase\nimport band.gosrock.api.refund.service.GetRefundsUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport band.gosrock.domain.domains.order.domain.RefundStatus\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport org.springdoc.core.annotations.ParameterObject\nimport org.springframework.data.domain.Pageable\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PatchMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RequestParam\nimport org.springframework.web.bind.annotation.RestController\n\n@SecurityRequirement(name = \"access-token\")\n@Tag(name = \"7-1. [환불관리]\")\n@RestController\n@RequestMapping(\"/api/v1/events/{eventId}/refunds\")\nclass RefundController(\n    private val getRefundsUseCase: GetRefundsUseCase,\n    private val completeRefundUseCase: CompleteRefundUseCase,\n) {\n\n    @Operation(summary = \"환불 목록 조회\")\n    @GetMapping\n    fun getRefunds(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @RequestParam(required = false) refundStatus: RefundStatus?,\n        @ParameterObject pageable: Pageable,\n    ): PageResponse<RefundResponse> =\n        getRefundsUseCase.execute(userId, eventId, refundStatus, pageable)\n\n    @Operation(summary = \"환불 상세 조회\")\n    @GetMapping(\"/{orderUuid}\")\n    fun getRefundDetail(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @PathVariable orderUuid: String,\n    ): RefundResponse =\n        getRefundsUseCase.getDetail(userId, eventId, orderUuid)\n\n    @Operation(summary = \"환불 확인 처리 (REFUND_COMPLETED)\")\n    @PatchMapping(\"/{orderUuid}/complete\")\n    fun completeRefund(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @PathVariable orderUuid: String,\n    ): RefundResponse =\n        completeRefundUseCase.execute(userId, eventId, orderUuid)\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/refund/dto/response/RefundResponse.kt",
    "content": "package band.gosrock.api.refund.dto.response\n\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.RefundStatus\nimport java.time.LocalDateTime\n\ndata class RefundResponse(\n    val orderId: String?,\n    val orderNo: String?,\n    val userName: String?,\n    val eventName: String?,\n    val eventId: Long?,\n    val ticketName: String?,\n    val totalAmount: Long,\n    val cancelReason: String?,\n    val refundStatus: RefundStatus,\n    val refundStatusChangedAt: LocalDateTime?,\n    val withDrawAt: LocalDateTime?,\n    val createdAt: LocalDateTime?,\n) {\n    companion object {\n        fun of(order: Order, userName: String?, eventName: String?): RefundResponse =\n            RefundResponse(\n                orderId = order.uuid,\n                orderNo = order.orderNo,\n                userName = userName,\n                eventName = eventName,\n                eventId = order.eventId,\n                ticketName = order.orderName,\n                totalAmount = order.getTotalPaymentPrice().longValue(),\n                cancelReason = order.cancelReason,\n                refundStatus = order.refundStatus,\n                refundStatusChangedAt = order.refundStatusChangedAt,\n                withDrawAt = order.withDrawAt,\n                createdAt = order.createdAt,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/refund/service/CompleteRefundUseCase.kt",
    "content": "package band.gosrock.api.refund.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.MANAGER\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.refund.dto.response.RefundResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.slf4j.LoggerFactory\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass CompleteRefundUseCase(\n    private val orderAdaptor: OrderAdaptor,\n    private val userAdaptor: UserAdaptor,\n    private val eventAdaptor: EventAdaptor,\n) {\n    private val log = LoggerFactory.getLogger(CompleteRefundUseCase::class.java)\n\n    @Transactional\n    @HostRolesAllowed(role = MANAGER, findHostFrom = EVENT_ID, applyTransaction = false)\n    fun execute(userId: Long, eventId: Long, orderUuid: String): RefundResponse {\n        log.info(\"[CompleteRefundUseCase][execute] 환불 완료 처리 userId={} eventId={} orderUuid={}\", userId, eventId, orderUuid)\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        order.completeRefund()\n\n        val userName = order.userId?.let {\n            runCatching { userAdaptor.queryUser(it).profile?.name }.getOrNull()\n        }\n        val eventName = order.eventId?.let {\n            runCatching { eventAdaptor.findById(it).eventBasic?.name }.getOrNull()\n        }\n        return RefundResponse.of(order, userName, eventName)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/refund/service/GetRefundsUseCase.kt",
    "content": "package band.gosrock.api.refund.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.GUEST\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.common.page.PageResponse\nimport band.gosrock.api.refund.dto.response.RefundResponse\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.domain.RefundStatus\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.data.domain.Pageable\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass GetRefundsUseCase(\n    private val orderAdaptor: OrderAdaptor,\n    private val userAdaptor: UserAdaptor,\n    private val eventAdaptor: EventAdaptor,\n) {\n\n    @HostRolesAllowed(role = GUEST, findHostFrom = EVENT_ID)\n    fun execute(\n        userId: Long,\n        eventId: Long,\n        refundStatus: RefundStatus?,\n        pageable: Pageable,\n    ): PageResponse<RefundResponse> {\n        val orderPage = orderAdaptor.findRefunds(eventId, refundStatus, null, pageable)\n\n        val userIds = orderPage.content.mapNotNull { it.userId }\n        val userMap = userAdaptor.findUserByIdIn(userIds).associateBy { it.id }\n\n        val eventName = runCatching { eventAdaptor.findById(eventId).eventBasic?.name }.getOrNull()\n\n        return PageResponse.of(orderPage.map { order ->\n            val userName = order.userId?.let { userMap[it]?.profile?.name }\n            RefundResponse.of(order, userName, eventName)\n        })\n    }\n\n    @HostRolesAllowed(role = GUEST, findHostFrom = EVENT_ID)\n    fun getDetail(userId: Long, eventId: Long, orderUuid: String): RefundResponse {\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        val userName = order.userId?.let {\n            runCatching { userAdaptor.queryUser(it).profile?.name }.getOrNull()\n        }\n        val eventName = order.eventId?.let {\n            runCatching { eventAdaptor.findById(it).eventBasic?.name }.getOrNull()\n        }\n        return RefundResponse.of(order, userName, eventName)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/slack/handler/event/EventContentChangeEventHandler.kt",
    "content": "package band.gosrock.api.slack.handler.event\n\nimport band.gosrock.domain.common.alarm.EventSlackAlarm\nimport band.gosrock.domain.common.events.event.EventContentChangeEvent\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.infrastructure.config.slack.SlackMessageProvider\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\nprivate val log = LoggerFactory.getLogger(EventContentChangeEventHandler::class.java)\n\n@Component\nclass EventContentChangeEventHandler(\n    private val hostAdaptor: HostAdaptor,\n    private val eventAdaptor: EventAdaptor,\n    private val slackMessageProvider: SlackMessageProvider,\n) {\n    @Async\n    @TransactionalEventListener(\n        classes = [EventContentChangeEvent::class],\n        phase = TransactionPhase.AFTER_COMMIT,\n    )\n    fun handle(eventContentChangeEvent: EventContentChangeEvent) {\n        val host = hostAdaptor.findById(eventContentChangeEvent.hostId)\n        val event = eventAdaptor.findById(eventContentChangeEvent.eventId)\n        val message = EventSlackAlarm.changeContentOf(event)\n        slackMessageProvider.sendMessage(host.slackUrl, message)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/slack/handler/event/EventCreationEventHandler.kt",
    "content": "package band.gosrock.api.slack.handler.event\n\nimport band.gosrock.domain.common.alarm.EventSlackAlarm\nimport band.gosrock.domain.common.events.event.EventCreationEvent\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.infrastructure.config.slack.SlackMessageProvider\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\nprivate val log = LoggerFactory.getLogger(EventCreationEventHandler::class.java)\n\n@Component\nclass EventCreationEventHandler(\n    private val hostAdaptor: HostAdaptor,\n    private val eventAdaptor: EventAdaptor,\n    private val slackMessageProvider: SlackMessageProvider,\n) {\n    @Async\n    @TransactionalEventListener(\n        classes = [EventCreationEvent::class],\n        phase = TransactionPhase.AFTER_COMMIT,\n    )\n    fun handle(eventCreationEvent: EventCreationEvent) {\n        val host = hostAdaptor.findById(eventCreationEvent.hostId!!)\n        val message = EventSlackAlarm.creationOf(eventCreationEvent.eventName!!)\n        slackMessageProvider.sendMessage(host.slackUrl, message)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/slack/handler/event/EventDeletionEventHandler.kt",
    "content": "package band.gosrock.api.slack.handler.event\n\nimport band.gosrock.domain.common.alarm.EventSlackAlarm\nimport band.gosrock.domain.common.events.event.EventDeletionEvent\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.infrastructure.config.slack.SlackMessageProvider\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\nprivate val log = LoggerFactory.getLogger(EventDeletionEventHandler::class.java)\n\n@Component\nclass EventDeletionEventHandler(\n    private val hostAdaptor: HostAdaptor,\n    private val slackMessageProvider: SlackMessageProvider,\n) {\n    @Async\n    @TransactionalEventListener(\n        classes = [EventDeletionEvent::class],\n        phase = TransactionPhase.AFTER_COMMIT,\n    )\n    fun handle(eventDeletionEvent: EventDeletionEvent) {\n        val host = hostAdaptor.findById(eventDeletionEvent.hostId)\n        val eventName = eventDeletionEvent.eventName\n        val message = EventSlackAlarm.deletionOf(eventName)\n        slackMessageProvider.sendMessage(host.slackUrl, message)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/slack/handler/event/EventStatusChangeEventHandler.kt",
    "content": "package band.gosrock.api.slack.handler.event\n\nimport band.gosrock.domain.common.alarm.EventSlackAlarm\nimport band.gosrock.domain.common.events.event.EventStatusChangeEvent\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.infrastructure.config.slack.SlackMessageProvider\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\nprivate val log = LoggerFactory.getLogger(EventStatusChangeEventHandler::class.java)\n\n@Component\nclass EventStatusChangeEventHandler(\n    private val hostAdaptor: HostAdaptor,\n    private val eventAdaptor: EventAdaptor,\n    private val slackMessageProvider: SlackMessageProvider,\n) {\n    @Async\n    @TransactionalEventListener(\n        classes = [EventStatusChangeEvent::class],\n        phase = TransactionPhase.AFTER_COMMIT,\n    )\n    fun handle(eventStatusChangeEvent: EventStatusChangeEvent) {\n        val host = hostAdaptor.findById(eventStatusChangeEvent.hostId)\n        val event = eventAdaptor.findById(eventStatusChangeEvent.eventId)\n        val message = EventSlackAlarm.changeStatusOf(event)\n        slackMessageProvider.sendMessage(host.slackUrl, message)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/slack/handler/host/HostRegisterSlackEventHandler.kt",
    "content": "package band.gosrock.api.slack.handler.host\n\nimport band.gosrock.domain.common.alarm.HostSlackAlarm\nimport band.gosrock.domain.common.events.host.HostRegisterSlackEvent\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.infrastructure.config.slack.SlackMessageProvider\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\nprivate val log = LoggerFactory.getLogger(HostRegisterSlackEventHandler::class.java)\n\n@Component\nclass HostRegisterSlackEventHandler(\n    private val hostAdaptor: HostAdaptor,\n    private val slackMessageProvider: SlackMessageProvider,\n) {\n    @Async\n    @TransactionalEventListener(\n        classes = [HostRegisterSlackEvent::class],\n        phase = TransactionPhase.AFTER_COMMIT,\n    )\n    fun handle(hostRegisterSlackEvent: HostRegisterSlackEvent) {\n        val host = hostAdaptor.findById(hostRegisterSlackEvent.hostId!!)\n        val message = HostSlackAlarm.slackRegistrationOf(host)\n        slackMessageProvider.sendMessage(host.slackUrl, message)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/slack/handler/host/HostUserDisabledEventHandler.kt",
    "content": "package band.gosrock.api.slack.handler.host\n\nimport band.gosrock.api.email.service.HostUserDisabledEmailService\nimport band.gosrock.domain.common.alarm.HostSlackAlarm\nimport band.gosrock.domain.common.events.host.HostUserDisabledEvent\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.infrastructure.config.slack.SlackMessageProvider\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\nprivate val log = LoggerFactory.getLogger(HostUserDisabledEventHandler::class.java)\n\n@Component\nclass HostUserDisabledEventHandler(\n    private val userAdaptor: UserAdaptor,\n    private val hostAdaptor: HostAdaptor,\n    private val hostUserDisabledEmailService: HostUserDisabledEmailService,\n    private val slackMessageProvider: SlackMessageProvider,\n) {\n    @Async\n    @TransactionalEventListener(\n        classes = [HostUserDisabledEvent::class],\n        phase = TransactionPhase.AFTER_COMMIT,\n    )\n    fun handle(hostUserDisabledEvent: HostUserDisabledEvent) {\n        val userId = hostUserDisabledEvent.userId!!\n        val user = userAdaptor.queryUser(userId)\n        val host = hostAdaptor.findById(hostUserDisabledEvent.hostId!!)\n        val hostName = hostUserDisabledEvent.hostName!!\n        val message = HostSlackAlarm.disabledOf(user)\n        hostUserDisabledEmailService.execute(user.toEmailUserInfo(), hostName)\n        slackMessageProvider.sendMessage(host.slackUrl, message)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/slack/handler/host/HostUserInvitationEventHandler.kt",
    "content": "package band.gosrock.api.slack.handler.host\n\nimport band.gosrock.api.email.service.HostUserInvitationEmailService\nimport band.gosrock.domain.common.events.host.HostUserInvitationEvent\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\nprivate val log = LoggerFactory.getLogger(HostUserInvitationEventHandler::class.java)\n\n@Component\nclass HostUserInvitationEventHandler(\n    private val userAdaptor: UserAdaptor,\n    private val invitationEmailService: HostUserInvitationEmailService,\n) {\n    @Async\n    @TransactionalEventListener(\n        classes = [HostUserInvitationEvent::class],\n        phase = TransactionPhase.AFTER_COMMIT,\n    )\n    fun handle(hostUserInvitationEvent: HostUserInvitationEvent) {\n        val userId = hostUserInvitationEvent.userId!!\n        val user = userAdaptor.queryUser(userId)\n        val role = hostUserInvitationEvent.role!!\n        val hostName = hostUserInvitationEvent.hostProfileVo!!.name!!\n        invitationEmailService.execute(user.toEmailUserInfo(), hostName, role)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/slack/handler/host/HostUserJoinEventHandler.kt",
    "content": "package band.gosrock.api.slack.handler.host\n\nimport band.gosrock.domain.common.alarm.HostSlackAlarm\nimport band.gosrock.domain.common.events.host.HostUserJoinEvent\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.infrastructure.config.slack.SlackMessageProvider\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\nprivate val log = LoggerFactory.getLogger(HostUserJoinEventHandler::class.java)\n\n@Component\nclass HostUserJoinEventHandler(\n    private val userAdaptor: UserAdaptor,\n    private val hostAdaptor: HostAdaptor,\n    private val slackMessageProvider: SlackMessageProvider,\n) {\n    @Async\n    @TransactionalEventListener(\n        classes = [HostUserJoinEvent::class],\n        phase = TransactionPhase.AFTER_COMMIT,\n    )\n    fun handle(hostUserJoinEvent: HostUserJoinEvent) {\n        val user = userAdaptor.queryUser(hostUserJoinEvent.userId!!)\n        val host = hostAdaptor.findById(hostUserJoinEvent.hostId!!)\n        val message = HostSlackAlarm.joinOf(host, user)\n        slackMessageProvider.sendMessage(host.slackUrl, message)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/slack/handler/host/HostUserRoleChangeEventHandler.kt",
    "content": "package band.gosrock.api.slack.handler.host\n\nimport band.gosrock.api.email.service.HostMasterChangeEmailService\nimport band.gosrock.api.email.service.HostUserRoleChangeEmailService\nimport band.gosrock.domain.common.alarm.HostSlackAlarm\nimport band.gosrock.domain.common.events.host.HostUserRoleChangeEvent\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.domain.HostRole\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.infrastructure.config.slack.SlackMessageProvider\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\nprivate val log = LoggerFactory.getLogger(HostUserRoleChangeEventHandler::class.java)\n\n@Component\nclass HostUserRoleChangeEventHandler(\n    private val userAdaptor: UserAdaptor,\n    private val hostAdaptor: HostAdaptor,\n    private val hostMasterChangeEmailService: HostMasterChangeEmailService,\n    private val hostUserRoleChangeEmailService: HostUserRoleChangeEmailService,\n    private val slackMessageProvider: SlackMessageProvider,\n) {\n    @Async\n    @TransactionalEventListener(\n        classes = [HostUserRoleChangeEvent::class],\n        phase = TransactionPhase.AFTER_COMMIT,\n    )\n    fun handle(hostUserRoleChangeEvent: HostUserRoleChangeEvent) {\n        val userId = hostUserRoleChangeEvent.userId!!\n        val user = userAdaptor.queryUser(userId)\n        val host = hostAdaptor.findById(hostUserRoleChangeEvent.hostId!!)\n        val role = hostUserRoleChangeEvent.role!!\n        val hostName = hostUserRoleChangeEvent.hostName!!\n        val message = HostSlackAlarm.changeMasterOf(host, user)\n\n        // role == master 이면 전체에게 추가 알림 + 이메일\n        if (role == HostRole.MASTER) {\n            // todo :: host users foreach\n            // todo :: 마스터 유저 권한 부여 api\n            hostMasterChangeEmailService.execute(user.toEmailUserInfo(), hostName, role)\n            slackMessageProvider.sendMessage(host.slackUrl, message)\n        } else {\n            hostUserRoleChangeEmailService.execute(user.toEmailUserInfo(), hostName, role)\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/slack/handler/order/DudoongTicketCancelOrderEventHandler.kt",
    "content": "package band.gosrock.api.slack.handler.order\n\nimport band.gosrock.domain.common.alarm.HostSlackAlarm\nimport band.gosrock.domain.common.events.order.WithDrawOrderEvent\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.infrastructure.config.slack.SlackMessageProvider\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.annotation.Transactional\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\n@Component\nclass DudoongTicketCancelOrderEventHandler(\n    private val eventAdaptor: EventAdaptor,\n    private val hostAdaptor: HostAdaptor,\n    private val orderAdaptor: OrderAdaptor,\n    private val slackMessageProvider: SlackMessageProvider,\n) {\n    private val log = LoggerFactory.getLogger(DudoongTicketCancelOrderEventHandler::class.java)\n\n    @Async\n    @TransactionalEventListener(classes = [WithDrawOrderEvent::class], phase = TransactionPhase.AFTER_COMMIT)\n    fun handle(withDrawOrderEvent: WithDrawOrderEvent) {\n        log.info(\"두둥 티켓 취소 전송되는 알림\")\n        if (!withDrawOrderEvent.isDudoongTicketOrder) return\n        if (withDrawOrderEvent.isRefund) return\n        val order = orderAdaptor.findByOrderUuid(withDrawOrderEvent.orderUuid)\n        val event = eventAdaptor.findById(order.eventId!!)\n        val host = hostAdaptor.findById(event.hostId!!)\n        val message = HostSlackAlarm.dudoongOrderCancel(event, order)\n        slackMessageProvider.sendMessage(host.slackUrl!!, message)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/slack/handler/order/DudoongTicketRefundOrderEventHandler.kt",
    "content": "package band.gosrock.api.slack.handler.order\n\nimport band.gosrock.domain.common.alarm.HostSlackAlarm\nimport band.gosrock.domain.common.events.order.WithDrawOrderEvent\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.infrastructure.config.slack.SlackMessageProvider\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.annotation.Transactional\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\n@Component\nclass DudoongTicketRefundOrderEventHandler(\n    private val eventAdaptor: EventAdaptor,\n    private val hostAdaptor: HostAdaptor,\n    private val orderAdaptor: OrderAdaptor,\n    private val slackMessageProvider: SlackMessageProvider,\n) {\n    private val log = LoggerFactory.getLogger(DudoongTicketRefundOrderEventHandler::class.java)\n\n    @Async\n    @TransactionalEventListener(classes = [WithDrawOrderEvent::class], phase = TransactionPhase.AFTER_COMMIT)\n    fun handle(withDrawOrderEvent: WithDrawOrderEvent) {\n        log.info(\"두둥 티켓 환불시 전송되는 알림\")\n        if (!withDrawOrderEvent.isDudoongTicketOrder) return\n        if (!withDrawOrderEvent.isRefund) return\n        val order = orderAdaptor.findByOrderUuid(withDrawOrderEvent.orderUuid)\n        val event = eventAdaptor.findById(order.eventId!!)\n        val host = hostAdaptor.findById(event.hostId!!)\n        val message = HostSlackAlarm.dudoongOrderRefund(event, order)\n        slackMessageProvider.sendMessage(host.slackUrl!!, message)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/slack/handler/order/NewApproveOrderAlarmEventHandler.kt",
    "content": "package band.gosrock.api.slack.handler.order\n\nimport band.gosrock.domain.common.alarm.HostSlackAlarm\nimport band.gosrock.domain.common.events.order.CreateOrderEvent\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.infrastructure.config.slack.SlackMessageProvider\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.annotation.Transactional\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\n@Component\nclass NewApproveOrderAlarmEventHandler(\n    private val eventAdaptor: EventAdaptor,\n    private val hostAdaptor: HostAdaptor,\n    private val orderAdaptor: OrderAdaptor,\n    private val slackMessageProvider: SlackMessageProvider,\n) {\n    private val log = LoggerFactory.getLogger(NewApproveOrderAlarmEventHandler::class.java)\n\n    @Async\n    @TransactionalEventListener(classes = [CreateOrderEvent::class], phase = TransactionPhase.AFTER_COMMIT)\n    fun handle(createOrderEvent: CreateOrderEvent) {\n        // 승인 방식의 결제만 알림 발송 대상.\n        if (createOrderEvent.orderMethod.isPayment()) return\n        log.info(\"승인 방식 요청 알림 전송\")\n        val order = orderAdaptor.findByOrderUuid(createOrderEvent.orderUuid)\n        val event = eventAdaptor.findById(order.eventId!!)\n        val host = hostAdaptor.findById(event.hostId!!)\n        val message = HostSlackAlarm.newApproveOrder(event, order)\n        slackMessageProvider.sendMessage(host.slackUrl!!, message)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/slack/handler/order/NewConfirmOrderAlarmEventHandler.kt",
    "content": "package band.gosrock.api.slack.handler.order\n\nimport band.gosrock.domain.common.alarm.HostSlackAlarm\nimport band.gosrock.domain.common.events.order.DoneOrderEvent\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.infrastructure.config.slack.SlackMessageProvider\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.annotation.Transactional\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\n@Component\nclass NewConfirmOrderAlarmEventHandler(\n    private val eventAdaptor: EventAdaptor,\n    private val hostAdaptor: HostAdaptor,\n    private val orderAdaptor: OrderAdaptor,\n    private val slackMessageProvider: SlackMessageProvider,\n) {\n    private val log = LoggerFactory.getLogger(NewConfirmOrderAlarmEventHandler::class.java)\n\n    @Async\n    @TransactionalEventListener(classes = [DoneOrderEvent::class], phase = TransactionPhase.AFTER_COMMIT)\n    fun handle(doneOrderEvent: DoneOrderEvent) {\n        // 선착순 방식의 결제만 알림 발송 대상.\n        if (!doneOrderEvent.orderMethod.isPayment()) return\n        log.info(\"선착순 방식의 결제만 알림 발송 대상 알림 전송\")\n        val order = orderAdaptor.findByOrderUuid(doneOrderEvent.orderUuid)\n        val event = eventAdaptor.findById(order.eventId!!)\n        val host = hostAdaptor.findById(event.hostId!!)\n        val message = HostSlackAlarm.newConfirmOrder(event, order)\n        slackMessageProvider.sendMessage(host.slackUrl!!, message)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/slack/handler/order/OrderApprovedAlarmEventHandler.kt",
    "content": "package band.gosrock.api.slack.handler.order\n\nimport band.gosrock.domain.common.alarm.HostSlackAlarm\nimport band.gosrock.domain.common.events.order.DoneOrderEvent\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.infrastructure.config.slack.SlackMessageProvider\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.annotation.Transactional\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\n@Component\nclass OrderApprovedAlarmEventHandler(\n    private val eventAdaptor: EventAdaptor,\n    private val hostAdaptor: HostAdaptor,\n    private val orderAdaptor: OrderAdaptor,\n    private val slackMessageProvider: SlackMessageProvider,\n) {\n    private val log = LoggerFactory.getLogger(OrderApprovedAlarmEventHandler::class.java)\n\n    @Async\n    @TransactionalEventListener(classes = [DoneOrderEvent::class], phase = TransactionPhase.AFTER_COMMIT)\n    fun handle(doneOrderEvent: DoneOrderEvent) {\n        if (doneOrderEvent.orderMethod.isPayment()) return\n        log.info(\"승인 방식 완료 시에 알림 전송\")\n        val order = orderAdaptor.findByOrderUuid(doneOrderEvent.orderUuid)\n        val event = eventAdaptor.findById(order.eventId!!)\n        val host = hostAdaptor.findById(event.hostId!!)\n        val message = HostSlackAlarm.approvedOrder(event, order)\n        slackMessageProvider.sendMessage(host.slackUrl!!, message)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/slack/handler/order/WithDrawOrderEventHandler.kt",
    "content": "package band.gosrock.api.slack.handler.order\n\nimport band.gosrock.domain.common.alarm.HostSlackAlarm\nimport band.gosrock.domain.common.events.order.WithDrawOrderEvent\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.infrastructure.config.slack.SlackMessageProvider\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.annotation.Transactional\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\n@Component\nclass WithDrawOrderEventHandler(\n    private val eventAdaptor: EventAdaptor,\n    private val hostAdaptor: HostAdaptor,\n    private val orderAdaptor: OrderAdaptor,\n    private val slackMessageProvider: SlackMessageProvider,\n) {\n    private val log = LoggerFactory.getLogger(WithDrawOrderEventHandler::class.java)\n\n    @Async\n    @TransactionalEventListener(classes = [WithDrawOrderEvent::class], phase = TransactionPhase.AFTER_COMMIT)\n    fun handle(withDrawOrderEvent: WithDrawOrderEvent) {\n        log.info(\"선착순 유료,무료  승인 무료 시에 전송되는 알람\")\n        if (withDrawOrderEvent.isDudoongTicketOrder) return\n\n        val order = orderAdaptor.findByOrderUuid(withDrawOrderEvent.orderUuid)\n        val event = eventAdaptor.findById(order.eventId!!)\n        val host = hostAdaptor.findById(event.hostId!!)\n        val message = HostSlackAlarm.withDrawOrder(event, order)\n        slackMessageProvider.sendMessage(host.slackUrl!!, message)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/slack/sender/SlackBootNotificationSender.kt",
    "content": "package band.gosrock.api.slack.sender\n\nimport band.gosrock.common.helper.SpringEnvironmentHelper\nimport band.gosrock.infrastructure.config.slack.SlackServiceNotificationProvider\nimport com.slack.api.model.block.Blocks\nimport com.slack.api.model.block.Blocks.divider\nimport com.slack.api.model.block.Blocks.section\nimport com.slack.api.model.block.LayoutBlock\nimport com.slack.api.model.block.composition.BlockCompositions.plainText\nimport com.slack.api.model.block.composition.MarkdownTextObject\nimport org.slf4j.LoggerFactory\nimport org.springframework.boot.context.event.ApplicationReadyEvent\nimport org.springframework.context.event.EventListener\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport java.net.InetAddress\nimport java.time.LocalDateTime\nimport java.time.format.DateTimeFormatter\n\n@Component\nclass SlackBootNotificationSender(\n    private val slackServiceNotificationProvider: SlackServiceNotificationProvider,\n    private val springEnvironmentHelper: SpringEnvironmentHelper,\n) {\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    @Async\n    @EventListener(ApplicationReadyEvent::class)\n    fun onApplicationReady() {\n        try {\n            val layoutBlocks = mutableListOf<LayoutBlock>()\n            layoutBlocks.add(Blocks.header { it.text(plainText(\"Application Started\")) })\n            layoutBlocks.add(divider())\n\n            val profile = resolveProfile()\n            val hostname = runCatching { InetAddress.getLocalHost().hostName }.getOrDefault(\"unknown\")\n            val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss\"))\n\n            val profileMarkdown = MarkdownTextObject.builder().text(\"* Profile :*\\n`$profile`\").build()\n            val hostMarkdown = MarkdownTextObject.builder().text(\"* Host :*\\n`$hostname`\").build()\n            layoutBlocks.add(section { it.fields(listOf(profileMarkdown, hostMarkdown)) })\n\n            val timeMarkdown = MarkdownTextObject.builder().text(\"* Started At :*\\n$timestamp\").build()\n            layoutBlocks.add(section { it.fields(listOf(timeMarkdown)) })\n\n            slackServiceNotificationProvider.sendNotification(layoutBlocks)\n            log.info(\"Boot notification sent to Slack (profile={})\", profile)\n        } catch (e: Exception) {\n            log.warn(\"Failed to send boot notification to Slack\", e)\n        }\n    }\n\n    private fun resolveProfile(): String = when {\n        springEnvironmentHelper.isProdProfile() -> \"prod\"\n        springEnvironmentHelper.isStagingProfile() -> \"staging\"\n        springEnvironmentHelper.isDevProfile() -> \"dev\"\n        else -> \"local\"\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/slack/sender/SlackInternalErrorSender.kt",
    "content": "package band.gosrock.api.slack.sender\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.slack.api.model.block.Blocks\nimport com.slack.api.model.block.Blocks.divider\nimport com.slack.api.model.block.Blocks.section\nimport com.slack.api.model.block.LayoutBlock\nimport com.slack.api.model.block.composition.BlockCompositions.plainText\nimport com.slack.api.model.block.composition.MarkdownTextObject\nimport band.gosrock.infrastructure.config.slack.SlackErrorNotificationProvider\nimport org.slf4j.LoggerFactory\nimport org.slf4j.MDC\nimport org.springframework.stereotype.Component\nimport org.springframework.web.util.ContentCachingRequestWrapper\nimport java.io.IOException\n\nprivate val log = LoggerFactory.getLogger(SlackInternalErrorSender::class.java)\n\n@Component\nclass SlackInternalErrorSender(\n    private val objectMapper: ObjectMapper,\n    private val slackProvider: SlackErrorNotificationProvider,\n) {\n    @Throws(IOException::class)\n    fun execute(cachingRequest: ContentCachingRequestWrapper, e: Exception, userId: Long) {\n        val url = cachingRequest.requestURL.toString()\n        val method = cachingRequest.method\n        val body = objectMapper.readTree(cachingRequest.contentAsByteArray).toString()\n        val errorMessage = e.message\n        val errorStack = slackProvider.getErrorStack(e)\n        val errorUserIP = cachingRequest.remoteAddr\n\n        val layoutBlocks = mutableListOf<LayoutBlock>()\n        layoutBlocks.add(\n            Blocks.header { it.text(plainText(\"Error Detection\")) }\n        )\n        layoutBlocks.add(divider())\n\n        val traceId = MDC.get(\"traceId\") ?: \"no-trace\"\n\n        val errorUserIdMarkdown = MarkdownTextObject.builder().text(\"* User Id :*\\n$userId\").build()\n        val errorUserIpMarkdown = MarkdownTextObject.builder().text(\"* User IP :*\\n$errorUserIP\").build()\n        layoutBlocks.add(section { it.fields(listOf(errorUserIdMarkdown, errorUserIpMarkdown)) })\n\n        val traceIdMarkdown = MarkdownTextObject.builder().text(\"* Trace ID :*\\n`$traceId`\").build()\n        layoutBlocks.add(section { it.fields(listOf(traceIdMarkdown)) })\n\n        val methodMarkdown = MarkdownTextObject.builder().text(\"* Request Addr :*\\n$method : $url\").build()\n        val bodyMarkdown = MarkdownTextObject.builder().text(\"* Request Body :*\\n$body\").build()\n        layoutBlocks.add(section { it.fields(listOf(methodMarkdown, bodyMarkdown)) })\n\n        layoutBlocks.add(divider())\n\n        val errorNameMarkdown = MarkdownTextObject.builder().text(\"* Message :*\\n$errorMessage\").build()\n        val errorStackMarkdown = MarkdownTextObject.builder().text(\"* Stack Trace :*\\n$errorStack\").build()\n        layoutBlocks.add(section { it.fields(listOf(errorNameMarkdown, errorStackMarkdown)) })\n\n        slackProvider.sendNotification(layoutBlocks)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/slack/sender/SlackThrottleErrorSender.kt",
    "content": "package band.gosrock.api.slack.sender\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.slack.api.model.block.Blocks\nimport com.slack.api.model.block.Blocks.divider\nimport com.slack.api.model.block.Blocks.section\nimport com.slack.api.model.block.LayoutBlock\nimport com.slack.api.model.block.composition.BlockCompositions.plainText\nimport com.slack.api.model.block.composition.MarkdownTextObject\nimport band.gosrock.infrastructure.config.slack.SlackErrorNotificationProvider\nimport org.slf4j.LoggerFactory\nimport org.slf4j.MDC\nimport org.springframework.stereotype.Component\nimport org.springframework.web.util.ContentCachingRequestWrapper\nimport java.io.IOException\n\nprivate val log = LoggerFactory.getLogger(SlackThrottleErrorSender::class.java)\n\n@Component\nclass SlackThrottleErrorSender(\n    private val objectMapper: ObjectMapper,\n    private val slackProvider: SlackErrorNotificationProvider,\n) {\n    @Throws(IOException::class)\n    fun execute(cachingRequest: ContentCachingRequestWrapper, userId: Long) {\n        val url = cachingRequest.requestURL.toString()\n        val method = cachingRequest.method\n        val body = objectMapper.readTree(cachingRequest.contentAsByteArray).toString()\n        val errorUserIP = cachingRequest.remoteAddr\n\n        val layoutBlocks = mutableListOf<LayoutBlock>()\n        layoutBlocks.add(\n            Blocks.header { it.text(plainText(\"Rate Limit Error\")) }\n        )\n        layoutBlocks.add(divider())\n\n        val traceId = MDC.get(\"traceId\") ?: \"no-trace\"\n\n        val errorUserIdMarkdown = MarkdownTextObject.builder().text(\"* User Id :*\\n$userId\").build()\n        val errorUserIpMarkdown = MarkdownTextObject.builder().text(\"* User IP :*\\n$errorUserIP\").build()\n        layoutBlocks.add(section { it.fields(listOf(errorUserIdMarkdown, errorUserIpMarkdown)) })\n\n        val traceIdMarkdown = MarkdownTextObject.builder().text(\"* Trace ID :*\\n`$traceId`\").build()\n        layoutBlocks.add(section { it.fields(listOf(traceIdMarkdown)) })\n\n        val methodMarkdown = MarkdownTextObject.builder().text(\"* Request Addr :*\\n$method : $url\").build()\n        val bodyMarkdown = MarkdownTextObject.builder().text(\"* Request Body :*\\n$body\").build()\n        layoutBlocks.add(section { it.fields(listOf(methodMarkdown, bodyMarkdown)) })\n\n        layoutBlocks.add(divider())\n\n        slackProvider.sendNotification(layoutBlocks)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/statistic/controller/AdminStatisticController.kt",
    "content": "package band.gosrock.api.statistic.controller\n\nimport band.gosrock.api.statistic.dto.DashBoardStatisticResponse\nimport band.gosrock.api.statistic.useCase.StatisticUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RestController\n\n@SecurityRequirement(name = \"access-token\")\n@Tag(name = \"a2. [이벤트관리] 통계관련\")\n@RestController\n@RequestMapping(\"/api/v1/events/{eventId}/statistics\")\nclass AdminStatisticController(\n    private val statisticUseCase: StatisticUseCase,\n) {\n    @Operation(summary = \"대시보드 통계를 불러옵니다.\")\n    @GetMapping\n    fun getStatistic(@CurrentUserId userId: Long, @PathVariable eventId: Long): DashBoardStatisticResponse =\n        statisticUseCase.execute(userId, eventId)\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/statistic/dto/DashBoardStatisticResponse.kt",
    "content": "package band.gosrock.api.statistic.dto\n\nimport band.gosrock.api.statistic.query.result.IssuedTicketStatistic\nimport band.gosrock.api.statistic.query.result.OrderStatistic\nimport com.fasterxml.jackson.annotation.JsonUnwrapped\n\nclass DashBoardStatisticResponse(\n    @JsonUnwrapped val orderStatistic: OrderStatistic,\n    @JsonUnwrapped val issuedTicketStatistic: IssuedTicketStatistic,\n) {\n    companion object {\n        @JvmStatic\n        fun of(\n            orderStatistic: OrderStatistic,\n            issuedTicketStatistic: IssuedTicketStatistic,\n        ): DashBoardStatisticResponse =\n            DashBoardStatisticResponse(\n                orderStatistic = orderStatistic,\n                issuedTicketStatistic = issuedTicketStatistic,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/statistic/query/IssuedTicketQueryRepository.kt",
    "content": "package band.gosrock.api.statistic.query\n\nimport band.gosrock.api.statistic.query.result.IssuedTicketStatistic\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketStatus.ENTRANCE_COMPLETED\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketStatus.ENTRANCE_INCOMPLETE\nimport band.gosrock.domain.domains.issuedTicket.domain.QIssuedTicket.issuedTicket\nimport com.querydsl.core.types.Expression\nimport com.querydsl.core.types.ExpressionUtils\nimport com.querydsl.core.types.ExpressionUtils.count\nimport com.querydsl.core.types.Projections\nimport com.querydsl.core.types.dsl.BooleanExpression\nimport com.querydsl.jpa.JPAExpressions\nimport com.querydsl.jpa.impl.JPAQueryFactory\nimport org.springframework.stereotype.Component\n\n@Component\nclass IssuedTicketQueryRepository(\n    private val queryFactory: JPAQueryFactory,\n) {\n    fun statistic(eventId: Long): IssuedTicketStatistic =\n        queryFactory\n            .select(\n                Projections.constructor(\n                    IssuedTicketStatistic::class.java,\n                    issuedCountEx(eventId),\n                    enteredCountEx(eventId),\n                ),\n            )\n            .from(issuedTicket)\n            .fetchFirst()\n            ?: IssuedTicketStatistic(0L, 0L)\n\n    private fun enteredCountEx(eventId: Long): Expression<Long> =\n        ExpressionUtils.`as`(\n            JPAExpressions.select(count(issuedTicket.id))\n                .from(issuedTicket)\n                .where(\n                    eventIdEq(eventId),\n                    issuedTicket.issuedTicketStatus.eq(ENTRANCE_COMPLETED),\n                ),\n            \"enteredCount\",\n        )\n\n    private fun issuedCountEx(eventId: Long): Expression<Long> =\n        ExpressionUtils.`as`(\n            JPAExpressions.select(count(issuedTicket.id))\n                .from(issuedTicket)\n                .where(\n                    eventIdEq(eventId),\n                    issuedTicket.issuedTicketStatus.`in`(ENTRANCE_COMPLETED, ENTRANCE_INCOMPLETE),\n                ),\n            \"issuedCount\",\n        )\n\n    private fun eventIdEq(eventId: Long?): BooleanExpression? =\n        if (eventId == null) null else issuedTicket.eventId.eq(eventId)\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/statistic/query/OrderQueryRepository.kt",
    "content": "package band.gosrock.api.statistic.query\n\nimport band.gosrock.api.statistic.query.result.OrderStatistic\nimport band.gosrock.domain.domains.order.domain.OrderStatus.APPROVED\nimport band.gosrock.domain.domains.order.domain.OrderStatus.CONFIRM\nimport band.gosrock.domain.domains.order.domain.OrderStatus.PENDING_APPROVE\nimport band.gosrock.domain.domains.order.domain.QOrder.order\nimport com.querydsl.core.types.Expression\nimport com.querydsl.core.types.ExpressionUtils\nimport com.querydsl.core.types.ExpressionUtils.count\nimport com.querydsl.core.types.Projections\nimport com.querydsl.core.types.dsl.BooleanExpression\nimport com.querydsl.jpa.JPAExpressions\nimport com.querydsl.jpa.impl.JPAQueryFactory\nimport java.math.BigDecimal\nimport org.springframework.stereotype.Component\n\n@Component\nclass OrderQueryRepository(\n    private val queryFactory: JPAQueryFactory,\n) {\n    fun statistic(eventId: Long): OrderStatistic =\n        queryFactory\n            .select(\n                Projections.constructor(\n                    OrderStatistic::class.java,\n                    doneCountEx(eventId),\n                    notApprovedCountEx(eventId),\n                    sellAmount(eventId),\n                ),\n            )\n            .from(order)\n            .fetchFirst()\n            ?: OrderStatistic(0L, 0L, BigDecimal.ZERO)\n\n    private fun doneCountEx(eventId: Long): Expression<Long> =\n        ExpressionUtils.`as`(\n            JPAExpressions.select(count(order.id))\n                .from(order)\n                .where(eventIdEq(eventId), order.orderStatus.`in`(CONFIRM, APPROVED)),\n            \"doneCount\",\n        )\n\n    private fun notApprovedCountEx(eventId: Long): Expression<Long> =\n        ExpressionUtils.`as`(\n            JPAExpressions.select(count(order.id))\n                .from(order)\n                .where(eventIdEq(eventId), order.orderStatus.eq(PENDING_APPROVE)),\n            \"notApprovedCount\",\n        )\n\n    private fun sellAmount(eventId: Long): Expression<BigDecimal> =\n        ExpressionUtils.`as`(\n            JPAExpressions.select(order.totalPaymentInfo.paymentAmount.amount.sum())\n                .from(order)\n                .where(eventIdEq(eventId), order.orderStatus.`in`(CONFIRM, APPROVED)),\n            \"sellAmount\",\n        )\n\n    private fun eventIdEq(eventId: Long?): BooleanExpression? =\n        if (eventId == null) null else order.eventId.eq(eventId)\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/statistic/query/result/IssuedTicketStatistic.kt",
    "content": "package band.gosrock.api.statistic.query.result\n\nclass IssuedTicketStatistic(issuedCount: Long, enteredCount: Long) {\n    val issuedCount: Long = issuedCount\n    val enteredCount: Long = enteredCount\n    val notEnteredCount: Long = issuedCount - enteredCount\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/statistic/query/result/OrderStatistic.kt",
    "content": "package band.gosrock.api.statistic.query.result\n\nimport band.gosrock.domain.common.vo.Money\nimport java.math.BigDecimal\n\nclass OrderStatistic(doneCount: Long, notApprovedCount: Long, sellAmount: BigDecimal?) {\n    val DoneCount: Long = doneCount\n    val notApprovedCount: Long = notApprovedCount\n    val sellAmount: Money = Money.wons((sellAmount ?: BigDecimal.ZERO).toLong())\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/statistic/useCase/StatisticUseCase.kt",
    "content": "package band.gosrock.api.statistic.useCase\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.GUEST\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.statistic.dto.DashBoardStatisticResponse\nimport band.gosrock.api.statistic.query.IssuedTicketQueryRepository\nimport band.gosrock.api.statistic.query.OrderQueryRepository\nimport band.gosrock.common.annotation.UseCase\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\n@Transactional(readOnly = true)\nclass StatisticUseCase(\n    private val issuedTicketQueryRepository: IssuedTicketQueryRepository,\n    private val orderQueryRepository: OrderQueryRepository,\n) {\n    @HostRolesAllowed(role = GUEST, findHostFrom = EVENT_ID)\n    fun execute(userId: Long, eventId: Long): DashBoardStatisticResponse =\n        DashBoardStatisticResponse.of(\n            orderQueryRepository.statistic(eventId),\n            issuedTicketQueryRepository.statistic(eventId),\n        )\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/controller/TicketItemController.kt",
    "content": "package band.gosrock.api.ticketItem.controller\n\nimport band.gosrock.api.ticketItem.dto.request.ApplyTicketOptionRequest\nimport band.gosrock.api.ticketItem.dto.request.CreateTicketItemRequest\nimport band.gosrock.api.ticketItem.dto.request.UnapplyTicketOptionRequest\nimport band.gosrock.api.ticketItem.dto.response.GetAppliedOptionGroupsResponse\nimport band.gosrock.api.ticketItem.dto.response.GetEventTicketItemsResponse\nimport band.gosrock.api.ticketItem.dto.response.GetTicketItemOptionsResponse\nimport band.gosrock.api.ticketItem.dto.response.TicketItemResponse\nimport band.gosrock.api.ticketItem.service.ApplyTicketOptionUseCase\nimport band.gosrock.api.ticketItem.service.DeleteTicketItemUseCase\nimport band.gosrock.api.ticketItem.service.GetAppliedOptionGroupsUseCase\nimport band.gosrock.api.ticketItem.service.GetEventTicketItemsUseCase\nimport band.gosrock.api.ticketItem.service.GetTicketOptionsUseCase\nimport band.gosrock.api.ticketItem.service.UnapplyTicketOptionUseCase\nimport band.gosrock.api.ticketItem.service.CreateTicketItemUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport band.gosrock.common.annotation.DisableSwaggerSecurity\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport jakarta.validation.Valid\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PatchMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestBody\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RestController\n\n@SecurityRequirement(name = \"access-token\")\n@Tag(name = \"7-1. [티켓상품]\")\n@RestController\n@RequestMapping(\"/api/v1/events/{eventId}/ticketItems\")\nclass TicketItemController(\n    val createTicketItemUseCase: CreateTicketItemUseCase,\n    val applyTicketOptionUseCase: ApplyTicketOptionUseCase,\n    val getTicketOptionsUseCase: GetTicketOptionsUseCase,\n    val getEventTicketItemsUseCase: GetEventTicketItemsUseCase,\n    val deleteTicketItemUseCase: DeleteTicketItemUseCase,\n    val getAppliedOptionGroupsUseCase: GetAppliedOptionGroupsUseCase,\n    val unapplyTicketOptionUseCase: UnapplyTicketOptionUseCase,\n) {\n\n    @Operation(\n        summary = \"특정 이벤트에 속하는 티켓 상품을 생성합니다.\",\n        description = \"두둥티켓은 승인형식만, 유료티켓은 선착순형식만 가능합니다.\",\n    )\n    @PostMapping\n    fun createTicketItem(\n        @CurrentUserId userId: Long,\n        @RequestBody @Valid createTicketItemRequest: CreateTicketItemRequest,\n        @PathVariable eventId: Long,\n    ): TicketItemResponse = createTicketItemUseCase.execute(userId, createTicketItemRequest, eventId)\n\n    @Operation(summary = \"옵션을 티켓상품에 적용합니다.\")\n    @PatchMapping(\"/{ticketItemId}/option\")\n    fun applyTicketOption(\n        @CurrentUserId userId: Long,\n        @RequestBody @Valid applyTicketOptionRequest: ApplyTicketOptionRequest,\n        @PathVariable eventId: Long,\n        @PathVariable ticketItemId: Long,\n    ): GetTicketItemOptionsResponse = applyTicketOptionUseCase.execute(userId, applyTicketOptionRequest, eventId, ticketItemId)\n\n    @Operation(summary = \"옵션을 티켓상품에 적용 취소합니다.\")\n    @PatchMapping(\"/{ticketItemId}/option/cancel\")\n    fun unapplyTicketOption(\n        @CurrentUserId userId: Long,\n        @RequestBody @Valid unapplyTicketOptionRequest: UnapplyTicketOptionRequest,\n        @PathVariable eventId: Long,\n        @PathVariable ticketItemId: Long,\n    ): GetTicketItemOptionsResponse = unapplyTicketOptionUseCase.execute(userId, unapplyTicketOptionRequest, eventId, ticketItemId)\n\n    @Operation(summary = \"해당 이벤트의 티켓상품을 모두 조회합니다.\")\n    @DisableSwaggerSecurity\n    @GetMapping\n    fun getEventTicketItems(@PathVariable eventId: Long): GetEventTicketItemsResponse =\n        getEventTicketItemsUseCase.execute(eventId)\n\n    @Operation(summary = \"해당 이벤트의 티켓상품을 모두 조회합니다. (어드민용)\", description = \"재고 정보가 무조건 공개됩니다.\")\n    @GetMapping(\"/admin\")\n    fun getEventTicketItemsForAdmin(@CurrentUserId userId: Long, @PathVariable eventId: Long): GetEventTicketItemsResponse =\n        getEventTicketItemsUseCase.executeForAdmin(userId, eventId)\n\n    @Operation(summary = \"해당 티켓상품의 옵션을 모두 조회합니다.\")\n    @GetMapping(\"/{ticketItemId}/options\")\n    fun getTicketItemOptions(\n        @PathVariable eventId: Long,\n        @PathVariable ticketItemId: Long,\n    ): GetTicketItemOptionsResponse = getTicketOptionsUseCase.execute(eventId, ticketItemId)\n\n    @Operation(summary = \"해당 이벤트의 티켓상품 옵션 적용 현황을 모두 조회합니다.\")\n    @GetMapping(\"/appliedOptionGroups\")\n    fun getAppliedOptionGroups(@PathVariable eventId: Long): GetAppliedOptionGroupsResponse =\n        getAppliedOptionGroupsUseCase.execute(eventId)\n\n    @Operation(summary = \"해당 티켓상품을 삭제합니다.\")\n    @PatchMapping(\"/{ticketItemId}\")\n    fun deleteTicketItem(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @PathVariable ticketItemId: Long,\n    ): GetEventTicketItemsResponse = deleteTicketItemUseCase.execute(userId, eventId, ticketItemId)\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/controller/TicketOptionController.kt",
    "content": "package band.gosrock.api.ticketItem.controller\n\nimport band.gosrock.api.ticketItem.dto.request.CreateTicketOptionRequest\nimport band.gosrock.api.ticketItem.dto.response.GetEventOptionsResponse\nimport band.gosrock.api.ticketItem.dto.response.OptionGroupResponse\nimport band.gosrock.api.ticketItem.service.CreateTicketOptionUseCase\nimport band.gosrock.api.ticketItem.service.DeleteOptionGroupUseCase\nimport band.gosrock.api.ticketItem.service.GetEventOptionsUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport jakarta.validation.Valid\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PatchMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestBody\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RestController\n\n@SecurityRequirement(name = \"access-token\")\n@Tag(name = \"7-2. [티켓상품옵션]\")\n@RestController\n@RequestMapping(\"/api/v1/events/{eventId}/ticketOptions\")\nclass TicketOptionController(\n    private val createTicketOptionUseCase: CreateTicketOptionUseCase,\n    private val getEventOptionsUseCase: GetEventOptionsUseCase,\n    private val deleteOptionGroupUseCase: DeleteOptionGroupUseCase,\n) {\n\n    @Operation(summary = \"해당 이벤트에 속하는 티켓옵션을 생성합니다.\")\n    @PostMapping\n    fun createTicketOption(\n        @CurrentUserId userId: Long,\n        @RequestBody @Valid createTicketOptionRequest: CreateTicketOptionRequest,\n        @PathVariable eventId: Long,\n    ): OptionGroupResponse = createTicketOptionUseCase.execute(userId, createTicketOptionRequest, eventId)\n\n    @Operation(summary = \"해당 이벤트에 속하는 옵션을 모두 조회합니다.\")\n    @GetMapping\n    fun getEventOptions(@PathVariable eventId: Long): GetEventOptionsResponse =\n        getEventOptionsUseCase.execute(eventId)\n\n    @Operation(summary = \"해당 옵션그룹을 삭제합니다.\")\n    @PatchMapping(\"/{optionGroupId}\")\n    fun deleteOptionGroup(\n        @CurrentUserId userId: Long,\n        @PathVariable eventId: Long,\n        @PathVariable optionGroupId: Long,\n    ): GetEventOptionsResponse = deleteOptionGroupUseCase.execute(userId, eventId, optionGroupId)\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/dto/request/ApplyTicketOptionRequest.kt",
    "content": "package band.gosrock.api.ticketItem.dto.request\n\nimport io.swagger.v3.oas.annotations.media.Schema\nimport jakarta.validation.constraints.Positive\n\ndata class ApplyTicketOptionRequest(\n    @field:Schema(nullable = false, example = \"1\")\n    @field:Positive(message = \"올바른 옵션그룹 고유 아이디를 입력해주세요\")\n    val optionGroupId: Long? = null,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/dto/request/CreateTicketItemRequest.kt",
    "content": "package band.gosrock.api.ticketItem.dto.request\n\nimport band.gosrock.common.annotation.Enum\nimport band.gosrock.domain.domains.ticket_item.domain.TicketPayType\nimport band.gosrock.domain.domains.ticket_item.domain.TicketType\nimport io.swagger.v3.oas.annotations.media.Schema\nimport jakarta.validation.constraints.NotEmpty\nimport jakarta.validation.constraints.NotNull\nimport jakarta.validation.constraints.Positive\n\ndata class CreateTicketItemRequest(\n    @field:Schema(nullable = false, defaultValue = \"두둥티켓\")\n    @field:Enum(message = \"두둥티켓, 무료티켓, 유료티켓만 허용됩니다\")\n    val payType: TicketPayType? = null,\n\n    @field:NotEmpty(message = \"티켓상품 이름을 입력해주세요\")\n    @field:Schema(nullable = false, example = \"일반 티켓\")\n    val name: String? = null,\n\n    @field:Schema(nullable = true, example = \"일반 입장 티켓입니다.\")\n    val description: String? = null,\n\n    @field:Schema(nullable = true, example = \"신한은행\")\n    val bankName: String? = null,\n\n    @field:Schema(nullable = true, example = \"110-123-1234567\")\n    val accountNumber: String? = null,\n\n    @field:Schema(nullable = true, example = \"김원진\")\n    val accountHolder: String? = null,\n\n    @field:NotNull\n    @field:Schema(defaultValue = \"0\", nullable = false, example = \"4000\")\n    val price: Long? = null,\n\n    @field:Positive\n    @field:Schema(nullable = false, example = \"100\")\n    val supplyCount: Long? = null,\n\n    @field:Schema(nullable = false, defaultValue = \"승인\")\n    @field:Enum(message = \"선착순, 승인만 허용됩니다\")\n    val approveType: TicketType? = null,\n\n    @field:NotNull\n    @field:Schema(nullable = false, example = \"true\")\n    val isQuantityPublic: Boolean? = null,\n\n    @field:NotNull\n    @field:Schema(nullable = false, example = \"1\")\n    val purchaseLimit: Long? = null,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/dto/request/CreateTicketOptionRequest.kt",
    "content": "package band.gosrock.api.ticketItem.dto.request\n\nimport band.gosrock.common.annotation.Enum\nimport band.gosrock.domain.domains.ticket_item.domain.OptionGroupType\nimport io.swagger.v3.oas.annotations.media.Schema\nimport jakarta.validation.constraints.NotEmpty\nimport jakarta.validation.constraints.NotNull\n\ndata class CreateTicketOptionRequest(\n    @field:Schema(nullable = false, defaultValue = \"Y/N\")\n    @field:Enum(message = \"Y/N, 주관식, 객관식만 허용됩니다\")\n    val type: OptionGroupType? = null,\n\n    @field:NotEmpty(message = \"옵션그룹 이름을 입력해주세요\")\n    @field:Schema(nullable = false, example = \"뒷풀이 참여 여부\")\n    val name: String? = null,\n\n    @field:Schema(nullable = true, example = \"공연이 끝난 후 오케이포차에서 진행하는 뒷풀이에 참여할 것인가요?\")\n    val description: String? = null,\n\n    @field:NotNull\n    @field:Schema(nullable = false, example = \"10000\")\n    val additionalPrice: Long? = null,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/dto/request/UnapplyTicketOptionRequest.kt",
    "content": "package band.gosrock.api.ticketItem.dto.request\n\nimport io.swagger.v3.oas.annotations.media.Schema\nimport jakarta.validation.constraints.Positive\n\ndata class UnapplyTicketOptionRequest(\n    @field:Schema(nullable = false, example = \"1\")\n    @field:Positive(message = \"올바른 옵션그룹 고유 아이디를 입력해주세요\")\n    val optionGroupId: Long? = null,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/dto/response/AppliedOptionGroupResponse.kt",
    "content": "package band.gosrock.api.ticketItem.dto.response\n\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport com.fasterxml.jackson.annotation.JsonUnwrapped\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class AppliedOptionGroupResponse(\n    @field:Schema(description = \"TicketItemResponse\")\n    @field:JsonUnwrapped\n    val ticketItemResponse: TicketItemResponse,\n\n    @field:Schema(description = \"적용된 옵션그룹 리스트\")\n    val optionGroups: List<OptionGroupResponse>,\n) {\n    companion object {\n        @JvmStatic\n        fun from(ticketItem: TicketItem, optionGroups: List<OptionGroupResponse>): AppliedOptionGroupResponse =\n            AppliedOptionGroupResponse(\n                ticketItemResponse = TicketItemResponse.from(ticketItem, true),\n                optionGroups = optionGroups,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/dto/response/ApplyTicketOptionResponse.kt",
    "content": "package band.gosrock.api.ticketItem.dto.response\n\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class ApplyTicketOptionResponse(\n    @field:Schema(description = \"티켓상품 id\")\n    val ticketItemId: Long?,\n\n    @field:Schema(description = \"옵션그룹 id 리스트\")\n    val optionGroupIds: List<Long>,\n) {\n    companion object {\n        @JvmStatic\n        fun from(ticketItem: TicketItem): ApplyTicketOptionResponse = ApplyTicketOptionResponse(\n            ticketItemId = ticketItem.id,\n            optionGroupIds = ticketItem.getOptionGroupIds(),\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/dto/response/GetAppliedOptionGroupsResponse.kt",
    "content": "package band.gosrock.api.ticketItem.dto.response\n\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class GetAppliedOptionGroupsResponse(\n    @field:Schema(description = \"적용 현황 리스트\")\n    val appliedOptionGroups: List<AppliedOptionGroupResponse>,\n) {\n    companion object {\n        @JvmStatic\n        fun from(appliedOptionGroups: List<AppliedOptionGroupResponse>): GetAppliedOptionGroupsResponse =\n            GetAppliedOptionGroupsResponse(appliedOptionGroups = appliedOptionGroups)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/dto/response/GetEventOptionsResponse.kt",
    "content": "package band.gosrock.api.ticketItem.dto.response\n\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class GetEventOptionsResponse(\n    @field:Schema(description = \"옵션그룹 리스트\")\n    val optionGroups: List<OptionGroupResponse>,\n) {\n    companion object {\n        @JvmStatic\n        fun from(optionGroups: List<OptionGroupResponse>): GetEventOptionsResponse =\n            GetEventOptionsResponse(optionGroups = optionGroups)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/dto/response/GetEventTicketItemsResponse.kt",
    "content": "package band.gosrock.api.ticketItem.dto.response\n\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class GetEventTicketItemsResponse(\n    @field:Schema(description = \"티켓상품 리스트\")\n    val ticketItems: List<TicketItemResponse>,\n) {\n    companion object {\n        @JvmStatic\n        fun from(ticketItems: List<TicketItemResponse>): GetEventTicketItemsResponse =\n            GetEventTicketItemsResponse(ticketItems = ticketItems)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/dto/response/GetTicketItemOptionsResponse.kt",
    "content": "package band.gosrock.api.ticketItem.dto.response\n\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class GetTicketItemOptionsResponse(\n    @field:Schema(description = \"옵션그룹 리스트\")\n    val optionGroups: List<OptionGroupResponse>,\n) {\n    companion object {\n        @JvmStatic\n        fun from(optionGroups: List<OptionGroupResponse>): GetTicketItemOptionsResponse =\n            GetTicketItemOptionsResponse(optionGroups = optionGroups)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/dto/response/OptionGroupResponse.kt",
    "content": "package band.gosrock.api.ticketItem.dto.response\n\nimport band.gosrock.domain.domains.ticket_item.domain.OptionGroup\nimport band.gosrock.domain.domains.ticket_item.domain.OptionGroupType\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class OptionGroupResponse(\n    @field:Schema(description = \"옵션그룹 id\")\n    val optionGroupId: Long?,\n\n    @field:Schema(description = \"옵션그룹 타입\")\n    val type: OptionGroupType?,\n\n    @field:Schema(description = \"이름\")\n    val name: String?,\n\n    @field:Schema(description = \"설명\")\n    val description: String?,\n\n    val options: List<OptionResponse>,\n) {\n    companion object {\n        @JvmStatic\n        fun from(optionGroup: OptionGroup): OptionGroupResponse = OptionGroupResponse(\n            optionGroupId = optionGroup.id,\n            type = optionGroup.type,\n            name = optionGroup.name,\n            description = optionGroup.description,\n            options = optionGroup.options.map { OptionResponse.from(it) },\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/dto/response/OptionResponse.kt",
    "content": "package band.gosrock.api.ticketItem.dto.response\n\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.ticket_item.domain.Option\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class OptionResponse(\n    @field:Schema(description = \"옵션 id\")\n    val optionId: Long?,\n\n    @field:Schema(description = \"응답\")\n    val answer: String?,\n\n    @field:Schema(description = \"추가 금액\")\n    val additionalPrice: Money?,\n) {\n    companion object {\n        @JvmStatic\n        fun from(option: Option): OptionResponse = OptionResponse(\n            optionId = option.id,\n            answer = option.answer,\n            additionalPrice = option.additionalPrice,\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/dto/response/TicketItemResponse.kt",
    "content": "package band.gosrock.api.ticketItem.dto.response\n\nimport band.gosrock.domain.common.vo.AccountInfoVo\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport band.gosrock.domain.domains.ticket_item.domain.TicketPayType\nimport band.gosrock.domain.domains.ticket_item.domain.TicketType\nimport io.swagger.v3.oas.annotations.media.Schema\n\ndata class TicketItemResponse(\n    @field:Schema(description = \"티켓상품 id\")\n    val ticketItemId: Long?,\n\n    @field:Schema(description = \"티켓 지불 타입\")\n    val payType: TicketPayType?,\n\n    @field:Schema(description = \"이름\")\n    val ticketName: String?,\n\n    @field:Schema(description = \"설명\")\n    val description: String?,\n\n    @field:Schema(description = \"가격\")\n    val price: Money?,\n\n    @field:Schema(description = \"티켓 승인 타입\")\n    val approveType: TicketType?,\n\n    @field:Schema(description = \"1인당 구매 제한 매수\")\n    val purchaseLimit: Long?,\n\n    @field:Schema(description = \"공급량\")\n    val supplyCount: Long?,\n\n    @field:Schema(description = \"재고\")\n    val quantity: Long?,\n\n    @field:Schema(description = \"재고공개 여부\")\n    val isQuantityPublic: Boolean?,\n\n    @field:Schema(description = \"계좌 정보\")\n    val accountInfo: AccountInfoVo?,\n\n    @field:Schema(description = \"재고가 감소한 티켓인지 리턴\")\n    val isSold: Boolean?,\n\n    @field:Schema(description = \"재고가 남아있는지 리턴\")\n    val isQuantityLeft: Boolean?,\n) {\n    companion object {\n        @JvmStatic\n        fun from(ticketItem: TicketItem, isAdmin: Boolean): TicketItemResponse = TicketItemResponse(\n            ticketItemId = ticketItem.id,\n            payType = ticketItem.payType,\n            ticketName = ticketItem.name,\n            description = ticketItem.description,\n            price = ticketItem.price,\n            approveType = ticketItem.type,\n            purchaseLimit = ticketItem.purchaseLimit,\n            supplyCount = ticketItem.supplyCount,\n            quantity = if (isAdmin || ticketItem.isQuantityPublic == true) ticketItem.quantity else null,\n            isQuantityPublic = ticketItem.isQuantityPublic,\n            accountInfo = ticketItem.accountInfo,\n            isSold = ticketItem.isSold(),\n            isQuantityLeft = ticketItem.isQuantityLeft(),\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/mapper/TicketItemMapper.kt",
    "content": "package band.gosrock.api.ticketItem.mapper\n\nimport band.gosrock.api.ticketItem.dto.request.CreateTicketItemRequest\nimport band.gosrock.api.ticketItem.dto.response.AppliedOptionGroupResponse\nimport band.gosrock.api.ticketItem.dto.response.GetAppliedOptionGroupsResponse\nimport band.gosrock.api.ticketItem.dto.response.GetEventTicketItemsResponse\nimport band.gosrock.api.ticketItem.dto.response.OptionGroupResponse\nimport band.gosrock.api.ticketItem.dto.response.TicketItemResponse\nimport band.gosrock.common.annotation.Mapper\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport org.springframework.transaction.annotation.Transactional\n\n@Mapper\nclass TicketItemMapper(\n    private val ticketItemAdaptor: TicketItemAdaptor,\n    private val eventAdaptor: EventAdaptor,\n) {\n\n    fun toTicketItem(createTicketItemRequest: CreateTicketItemRequest, eventId: Long): TicketItem =\n        TicketItem(\n            payType = createTicketItemRequest.payType,\n            name = createTicketItemRequest.name,\n            description = createTicketItemRequest.description,\n            price = Money.wons(createTicketItemRequest.price!!),\n            quantity = createTicketItemRequest.supplyCount,\n            supplyCount = createTicketItemRequest.supplyCount,\n            purchaseLimit = createTicketItemRequest.purchaseLimit,\n            type = createTicketItemRequest.approveType,\n            bankName = createTicketItemRequest.bankName,\n            accountNumber = createTicketItemRequest.accountNumber,\n            accountHolder = createTicketItemRequest.accountHolder,\n            isQuantityPublic = createTicketItemRequest.isQuantityPublic,\n            isSellable = true,\n            eventId = eventId,\n        )\n\n    @Transactional(readOnly = true)\n    fun toGetEventTicketItemsResponse(eventId: Long, isAdmin: Boolean): GetEventTicketItemsResponse {\n        val event = eventAdaptor.findById(eventId)\n        val ticketItems = ticketItemAdaptor.findAllByEventId(event.id!!)\n        return GetEventTicketItemsResponse.from(\n            ticketItems.map { TicketItemResponse.from(it, isAdmin) }\n        )\n    }\n\n    @Transactional(readOnly = true)\n    fun toGetAppliedOptionGroupsResponse(eventId: Long): GetAppliedOptionGroupsResponse {\n        val event = eventAdaptor.findById(eventId)\n        val ticketItems = ticketItemAdaptor.findAllByEventId(event.id!!)\n        val appliedOptionGroups = ticketItems.map { ticketItem ->\n            AppliedOptionGroupResponse.from(\n                ticketItem,\n                ticketItem.itemOptionGroups\n                    .mapNotNull { it.optionGroup }\n                    .map { OptionGroupResponse.from(it) }\n            )\n        }\n        return GetAppliedOptionGroupsResponse.from(appliedOptionGroups)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/mapper/TicketOptionMapper.kt",
    "content": "package band.gosrock.api.ticketItem.mapper\n\nimport band.gosrock.api.ticketItem.dto.request.CreateTicketOptionRequest\nimport band.gosrock.api.ticketItem.dto.response.GetEventOptionsResponse\nimport band.gosrock.api.ticketItem.dto.response.GetTicketItemOptionsResponse\nimport band.gosrock.api.ticketItem.dto.response.OptionGroupResponse\nimport band.gosrock.common.annotation.Mapper\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.ticket_item.adaptor.OptionGroupAdaptor\nimport band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor\nimport band.gosrock.domain.domains.ticket_item.domain.OptionGroup\nimport org.springframework.transaction.annotation.Transactional\n\n@Mapper\nclass TicketOptionMapper(\n    private val ticketItemAdaptor: TicketItemAdaptor,\n    private val eventAdaptor: EventAdaptor,\n    private val optionGroupAdaptor: OptionGroupAdaptor,\n) {\n\n    fun toOptionGroup(createTicketOptionRequest: CreateTicketOptionRequest, eventId: Long): OptionGroup =\n        OptionGroup(\n            eventId = eventId,\n            type = createTicketOptionRequest.type,\n            name = createTicketOptionRequest.name,\n            description = createTicketOptionRequest.description,\n            isEssential = true,\n            initialOptions = emptyList(),\n        )\n\n    @Transactional(readOnly = true)\n    fun toGetTicketItemOptionResponse(eventId: Long, ticketItemId: Long): GetTicketItemOptionsResponse {\n        val ticketItem = ticketItemAdaptor.queryTicketItem(ticketItemId)\n        ticketItem.validateEventId(eventId)\n        val optionGroups = ticketItem.itemOptionGroups.mapNotNull { it.optionGroup }\n        return GetTicketItemOptionsResponse.from(optionGroups.map { OptionGroupResponse.from(it) })\n    }\n\n    @Transactional(readOnly = true)\n    fun toGetEventOptionResponse(eventId: Long): GetEventOptionsResponse {\n        val event = eventAdaptor.findById(eventId)\n        val optionGroups = optionGroupAdaptor.findAllByEventId(event.id!!)\n        return GetEventOptionsResponse.from(optionGroups.map { OptionGroupResponse.from(it) })\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/service/ApplyTicketOptionUseCase.kt",
    "content": "package band.gosrock.api.ticketItem.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.GUEST\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.ticketItem.dto.request.ApplyTicketOptionRequest\nimport band.gosrock.api.ticketItem.dto.response.GetTicketItemOptionsResponse\nimport band.gosrock.api.ticketItem.mapper.TicketOptionMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.ticket_item.service.ItemOptionGroupService\n\n@UseCase\nclass ApplyTicketOptionUseCase(\n    private val itemOptionGroupService: ItemOptionGroupService,\n    private val ticketOptionMapper: TicketOptionMapper,\n) {\n\n    @HostRolesAllowed(role = GUEST, findHostFrom = EVENT_ID, applyTransaction = false)\n    fun execute(\n        userId: Long,\n        applyTicketOptionRequest: ApplyTicketOptionRequest,\n        eventId: Long,\n        ticketItemId: Long,\n    ): GetTicketItemOptionsResponse {\n        val optionGroupId = applyTicketOptionRequest.optionGroupId!!\n        val ticketItem = itemOptionGroupService.addItemOptionGroup(ticketItemId, optionGroupId, eventId)\n        return ticketOptionMapper.toGetTicketItemOptionResponse(eventId, ticketItem.id!!)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/service/CreateTicketItemUseCase.kt",
    "content": "package band.gosrock.api.ticketItem.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.GUEST\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.ticketItem.dto.request.CreateTicketItemRequest\nimport band.gosrock.api.ticketItem.dto.response.TicketItemResponse\nimport band.gosrock.api.ticketItem.mapper.TicketItemMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.ticket_item.service.TicketItemService\n\n@UseCase\nclass CreateTicketItemUseCase(\n    private val eventAdaptor: EventAdaptor,\n    private val hostAdaptor: HostAdaptor,\n    private val ticketItemService: TicketItemService,\n    private val ticketItemMapper: TicketItemMapper,\n) {\n\n    @HostRolesAllowed(role = GUEST, findHostFrom = EVENT_ID, applyTransaction = false)\n    fun execute(userId: Long, createTicketItemRequest: CreateTicketItemRequest, eventId: Long): TicketItemResponse {\n        val event = eventAdaptor.findById(eventId)\n        val host = hostAdaptor.findById(event.hostId!!)\n        val isPartner = host.partner\n        val ticketItem = ticketItemService.createTicketItem(\n            ticketItemMapper.toTicketItem(createTicketItemRequest, eventId),\n            isPartner,\n        )\n        return TicketItemResponse.from(ticketItem, true)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/service/CreateTicketOptionUseCase.kt",
    "content": "package band.gosrock.api.ticketItem.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.GUEST\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.ticketItem.dto.request.CreateTicketOptionRequest\nimport band.gosrock.api.ticketItem.dto.response.OptionGroupResponse\nimport band.gosrock.api.ticketItem.mapper.TicketOptionMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.ticket_item.service.TicketOptionService\n\n@UseCase\nclass CreateTicketOptionUseCase(\n    private val ticketOptionMapper: TicketOptionMapper,\n    private val ticketOptionService: TicketOptionService,\n) {\n\n    @HostRolesAllowed(role = GUEST, findHostFrom = EVENT_ID, applyTransaction = false)\n    fun execute(userId: Long, createTicketOptionRequest: CreateTicketOptionRequest, eventId: Long): OptionGroupResponse {\n        val ticketOption = ticketOptionMapper\n            .toOptionGroup(createTicketOptionRequest, eventId)\n            .createTicketOption(Money.wons(createTicketOptionRequest.additionalPrice!!))\n        val ticketOptionResult = ticketOptionService.createTicketOption(ticketOption)\n        return OptionGroupResponse.from(ticketOptionResult)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/service/DeleteOptionGroupUseCase.kt",
    "content": "package band.gosrock.api.ticketItem.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.GUEST\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.ticketItem.dto.response.GetEventOptionsResponse\nimport band.gosrock.api.ticketItem.mapper.TicketOptionMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.ticket_item.service.TicketOptionService\n\n@UseCase\nclass DeleteOptionGroupUseCase(\n    private val ticketOptionMapper: TicketOptionMapper,\n    private val ticketOptionService: TicketOptionService,\n) {\n\n    @HostRolesAllowed(role = GUEST, findHostFrom = EVENT_ID, applyTransaction = false)\n    fun execute(userId: Long, eventId: Long, optionGroupId: Long): GetEventOptionsResponse {\n        ticketOptionService.softDeleteOptionGroup(eventId, optionGroupId)\n        return ticketOptionMapper.toGetEventOptionResponse(eventId)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/service/DeleteTicketItemUseCase.kt",
    "content": "package band.gosrock.api.ticketItem.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.GUEST\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.ticketItem.dto.response.GetEventTicketItemsResponse\nimport band.gosrock.api.ticketItem.mapper.TicketItemMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.ticket_item.service.TicketItemService\n\n@UseCase\nclass DeleteTicketItemUseCase(\n    private val ticketItemMapper: TicketItemMapper,\n    private val ticketItemService: TicketItemService,\n) {\n\n    @HostRolesAllowed(role = GUEST, findHostFrom = EVENT_ID, applyTransaction = false)\n    fun execute(userId: Long, eventId: Long, ticketItemId: Long): GetEventTicketItemsResponse {\n        ticketItemService.softDeleteTicketItem(eventId, ticketItemId)\n        return ticketItemMapper.toGetEventTicketItemsResponse(eventId, true)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/service/GetAppliedOptionGroupsUseCase.kt",
    "content": "package band.gosrock.api.ticketItem.service\n\nimport band.gosrock.api.ticketItem.dto.response.GetAppliedOptionGroupsResponse\nimport band.gosrock.api.ticketItem.mapper.TicketItemMapper\nimport band.gosrock.common.annotation.UseCase\n\n@UseCase\nclass GetAppliedOptionGroupsUseCase(\n    private val ticketItemMapper: TicketItemMapper,\n) {\n\n    fun execute(eventId: Long): GetAppliedOptionGroupsResponse =\n        ticketItemMapper.toGetAppliedOptionGroupsResponse(eventId)\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/service/GetEventOptionsUseCase.kt",
    "content": "package band.gosrock.api.ticketItem.service\n\nimport band.gosrock.api.ticketItem.dto.response.GetEventOptionsResponse\nimport band.gosrock.api.ticketItem.mapper.TicketOptionMapper\nimport band.gosrock.common.annotation.UseCase\n\n@UseCase\nclass GetEventOptionsUseCase(\n    private val ticketOptionMapper: TicketOptionMapper,\n) {\n\n    fun execute(eventId: Long): GetEventOptionsResponse =\n        ticketOptionMapper.toGetEventOptionResponse(eventId)\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/service/GetEventTicketItemsUseCase.kt",
    "content": "package band.gosrock.api.ticketItem.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.GUEST\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.ticketItem.dto.response.GetEventTicketItemsResponse\nimport band.gosrock.api.ticketItem.mapper.TicketItemMapper\nimport band.gosrock.common.annotation.UseCase\n\n@UseCase\nclass GetEventTicketItemsUseCase(\n    private val ticketItemMapper: TicketItemMapper,\n) {\n\n    fun execute(eventId: Long): GetEventTicketItemsResponse =\n        ticketItemMapper.toGetEventTicketItemsResponse(eventId, false)\n\n    @HostRolesAllowed(role = GUEST, findHostFrom = EVENT_ID)\n    fun executeForAdmin(userId: Long, eventId: Long): GetEventTicketItemsResponse =\n        ticketItemMapper.toGetEventTicketItemsResponse(eventId, true)\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/service/GetTicketOptionsUseCase.kt",
    "content": "package band.gosrock.api.ticketItem.service\n\nimport band.gosrock.api.ticketItem.dto.response.GetTicketItemOptionsResponse\nimport band.gosrock.api.ticketItem.mapper.TicketOptionMapper\nimport band.gosrock.common.annotation.UseCase\n\n@UseCase\nclass GetTicketOptionsUseCase(\n    private val ticketOptionMapper: TicketOptionMapper,\n) {\n\n    fun execute(eventId: Long, ticketItemId: Long): GetTicketItemOptionsResponse =\n        ticketOptionMapper.toGetTicketItemOptionResponse(eventId, ticketItemId)\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/ticketItem/service/UnapplyTicketOptionUseCase.kt",
    "content": "package band.gosrock.api.ticketItem.service\n\nimport band.gosrock.api.common.aop.hostRole.FindHostFrom.EVENT_ID\nimport band.gosrock.api.common.aop.hostRole.HostQualification.GUEST\nimport band.gosrock.api.common.aop.hostRole.HostRolesAllowed\nimport band.gosrock.api.ticketItem.dto.request.UnapplyTicketOptionRequest\nimport band.gosrock.api.ticketItem.dto.response.GetTicketItemOptionsResponse\nimport band.gosrock.api.ticketItem.mapper.TicketOptionMapper\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.ticket_item.service.ItemOptionGroupService\n\n@UseCase\nclass UnapplyTicketOptionUseCase(\n    private val itemOptionGroupService: ItemOptionGroupService,\n    private val ticketOptionMapper: TicketOptionMapper,\n) {\n\n    @HostRolesAllowed(role = GUEST, findHostFrom = EVENT_ID, applyTransaction = false)\n    fun execute(\n        userId: Long,\n        unapplyTicketOptionRequest: UnapplyTicketOptionRequest,\n        eventId: Long,\n        ticketItemId: Long,\n    ): GetTicketItemOptionsResponse {\n        val optionGroupId = unapplyTicketOptionRequest.optionGroupId!!\n        val ticketItem = itemOptionGroupService.removeItemOptionGroup(ticketItemId, optionGroupId, eventId)\n        return ticketOptionMapper.toGetTicketItemOptionResponse(eventId, ticketItem.id!!)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/user/controller/UserController.kt",
    "content": "package band.gosrock.api.user.controller\n\nimport band.gosrock.api.user.model.dto.request.ChangeNameRequest\nimport band.gosrock.api.user.service.ChangeNameUseCase\nimport band.gosrock.common.annotation.CurrentUserId\nimport band.gosrock.api.user.service.MarketingUserUseCase\nimport band.gosrock.api.user.service.ReadUserUseCase\nimport band.gosrock.domain.common.vo.UserInfoVo\nimport io.swagger.v3.oas.annotations.Operation\nimport io.swagger.v3.oas.annotations.security.SecurityRequirement\nimport io.swagger.v3.oas.annotations.tags.Tag\nimport jakarta.validation.Valid\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PatchMapping\nimport org.springframework.web.bind.annotation.RequestBody\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RestController\n\n@RestController\n@RequestMapping(\"/api/v1/users\")\n@SecurityRequirement(name = \"access-token\")\n@Tag(name = \"2. [유저]\")\nclass UserController(\n    private val readUserUseCase: ReadUserUseCase,\n    private val marketingUserUseCase: MarketingUserUseCase,\n    private val changeNameUseCase: ChangeNameUseCase,\n) {\n\n    @Operation(summary = \"내 유저 정보를 불러 옵니다.\")\n    @GetMapping(\"/me\")\n    fun getMyUserInfo(@CurrentUserId userId: Long): UserInfoVo {\n        return readUserUseCase.execute(userId)\n    }\n\n    @Operation(summary = \"메일 동의 여부를 토글링 합니다\")\n    @PatchMapping(\"/me/mail\")\n    fun toggleMailReceiveAgree(@CurrentUserId userId: Long): UserInfoVo {\n        return marketingUserUseCase.toggleMailAgree(userId)\n    }\n\n    @Operation(summary = \"마케팅 동의 여부를 토글링 합니다\")\n    @PatchMapping(\"/me/marketing\")\n    fun toggleMarketingAgree(@CurrentUserId userId: Long): UserInfoVo {\n        return marketingUserUseCase.toggleMarketAgree(userId)\n    }\n\n    @Operation(summary = \"내 닉네임 변경\")\n    @PatchMapping(\"/me/name\")\n    fun changeMyName(\n        @CurrentUserId userId: Long,\n        @Valid @RequestBody request: ChangeNameRequest,\n    ) {\n        changeNameUseCase.execute(userId, request)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/user/model/dto/request/ChangeNameRequest.kt",
    "content": "package band.gosrock.api.user.model.dto.request\n\nimport jakarta.validation.constraints.NotBlank\nimport jakarta.validation.constraints.Size\n\ndata class ChangeNameRequest(\n    @field:NotBlank(message = \"이름을 입력해주세요.\")\n    @field:Size(min = 2, max = 7, message = \"이름은 2~7자여야 합니다.\")\n    val name: String,\n)\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/user/service/ChangeNameUseCase.kt",
    "content": "package band.gosrock.api.user.service\n\nimport band.gosrock.api.user.model.dto.request.ChangeNameRequest\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@UseCase\nclass ChangeNameUseCase(\n    private val userAdaptor: UserAdaptor,\n) {\n\n    @Transactional\n    fun execute(userId: Long, request: ChangeNameRequest) {\n        val user = userAdaptor.queryUser(userId)\n        user.changeName(request.name)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/user/service/MarketingUserUseCase.kt",
    "content": "package band.gosrock.api.user.service\n\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.common.vo.UserInfoVo\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.domain.domains.user.service.UserDomainService\n\n@UseCase\nclass MarketingUserUseCase(\n    private val userAdaptor: UserAdaptor,\n    private val userDomainService: UserDomainService,\n) {\n\n    fun execute(userId: Long): UserInfoVo {\n        val currentUser = userAdaptor.queryUser(userId)\n        return currentUser.toUserInfoVo()\n    }\n\n    fun toggleMailAgree(userId: Long): UserInfoVo {\n        userDomainService.toggleMailAgree(userId)\n        return userAdaptor.queryUser(userId).toUserInfoVo()\n    }\n\n    fun toggleMarketAgree(userId: Long): UserInfoVo {\n        userDomainService.toggleMarketAgree(userId)\n        return userAdaptor.queryUser(userId).toUserInfoVo()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/kotlin/band/gosrock/api/user/service/ReadUserUseCase.kt",
    "content": "package band.gosrock.api.user.service\n\nimport band.gosrock.common.annotation.UseCase\nimport band.gosrock.domain.common.vo.UserInfoVo\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\n\n@UseCase\nclass ReadUserUseCase(\n    private val userAdaptor: UserAdaptor,\n) {\n\n    fun execute(userId: Long): UserInfoVo {\n        val currentUser = userAdaptor.queryUser(userId)\n        return currentUser.toUserInfoVo()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/main/resources/application-local.yml",
    "content": "swagger:\n  user: user\n  password: password\n\nthrottle:\n  overdraft: 9999\n  greedyRefill: 9999\n\nacl.whiteList: 127.0.0.1,127.0.0.2\n\nlogging:\n  level:\n    band.gosrock: DEBUG\n"
  },
  {
    "path": "DuDoong-Api/src/main/resources/application.yml",
    "content": "# commons\napi:\n  prefix : ${API_PREFIX:/api}\n\nlogging:\n  pattern:\n    console: \"[%d{HH:mm:ss.SSS}] [%X{traceId:-no-trace}] [%-5level] %logger{36} - %msg%n\"\nspring:\n  profiles:\n    include:\n      - infrastructure\n      - domain\n      - common\n    group:\n      local: infrastructure, domain-local, common-local\n  autoconfigure:\n    exclude: org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration\nspringdoc:\n  default-produces-media-type: application/json\n  default-consumes-media-type: application/json\n  swagger-ui:\n    tags-sorter: alpha\n\nserver:\n  forward-headers-strategy: framework\n\nthrottle:\n  overdraft: ${RATE_LIMIT_OVERDRAFT:60}\n  greedyRefill: ${RATE_LIMIT_REFILL:60}\n\nacl.whiteList : ${ACL_WHITELIST:127.0.0.1,127.0.0.2}\n---\nspring:\n  config:\n    activate:\n      on-profile: dev\n#logging:\n#  level:\n#    root: info\n#logging:\n#  level:\n#    org.springframework.data.*.*: debug\n#    org.springframework.cache.*: debug\n---\nspring:\n  config:\n    activate:\n      on-profile: staging\nspringdoc:\n  api-docs:\n    enabled: false\n  swagger-ui:\n    enabled: false\n\n---\nspring:\n  config:\n    activate:\n      on-profile: prod\nspringdoc:\n  api-docs:\n    enabled: false\n  swagger-ui:\n    enabled: false\nlogging:\n  level:\n    ROOT: WARN\n"
  },
  {
    "path": "DuDoong-Api/src/test/java/band/gosrock/DuDoongApiServerApplication.java",
    "content": "package band.gosrock;\n\n\nimport band.gosrock.api.supports.ApiIntegrateSpringBootTest;\nimport org.junit.jupiter.api.Test;\n\n@ApiIntegrateSpringBootTest\npublic class DuDoongApiServerApplication {\n    @Test\n    void contextLoads() {}\n}\n"
  },
  {
    "path": "DuDoong-Api/src/test/java/band/gosrock/api/email/RegisterUserEventHandlerTest.java",
    "content": "package band.gosrock.api.email;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.times;\n\nimport band.gosrock.api.email.handler.RegisterUserEventEmailHandler;\nimport band.gosrock.api.supports.ApiIntegrateSpringBootTest;\nimport band.gosrock.domain.domains.user.domain.OauthInfo;\nimport band.gosrock.domain.domains.user.domain.OauthProvider;\nimport band.gosrock.domain.domains.user.domain.Profile;\nimport band.gosrock.domain.domains.user.service.UserDomainService;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.BDDMockito;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.mock.mockito.MockBean;\n\n@ApiIntegrateSpringBootTest\nclass RegisterUserEventHandlerTest {\n\n    @Autowired UserDomainService userDomainService;\n    @MockBean RegisterUserEventEmailHandler registerUserEventHandler;\n\n    @Test\n    void 유저등록시도메인이벤트가발생해야한다() {\n        // given\n        Profile profile = new Profile(\"test\", \"test@test.com\", null, null);\n        OauthInfo oauthInfo = new OauthInfo(OauthProvider.KAKAO, \"test-oid\");\n        // when\n        userDomainService.registerUser(profile, oauthInfo, Boolean.TRUE);\n\n        // then\n        BDDMockito.then(registerUserEventHandler).should(times(1)).handleRegisterUserEvent(any());\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/test/java/band/gosrock/api/supports/ApiIntegrateProfileResolver.java",
    "content": "package band.gosrock.api.supports;\n\n\nimport org.springframework.test.context.ActiveProfilesResolver;\n\n/**\n * activeProfile 의 Resolver 를 지정 통합테스트에 필요한 properties 인 common, infrastructure , domain 을 지정하기 위함.\n */\npublic class ApiIntegrateProfileResolver implements ActiveProfilesResolver {\n\n    @Override\n    public String[] resolve(Class<?> testClass) {\n        // some code to find out your active profiles\n        return new String[] {\"common\", \"infrastructure\", \"domain\"};\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/test/java/band/gosrock/api/supports/ApiIntegrateSpringBootTest.java",
    "content": "package band.gosrock.api.supports;\n\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.ActiveProfiles;\n\n/** 도메인 모듈의 통합테스트의 편의성을 위해서 만든 어노테이션 -이찬진 */\n@Target({ElementType.TYPE})\n@Retention(RetentionPolicy.RUNTIME)\n@SpringBootTest(classes = ApiIntegrateTestConfig.class)\n@ActiveProfiles(resolver = ApiIntegrateProfileResolver.class)\n@Documented\npublic @interface ApiIntegrateSpringBootTest {}\n"
  },
  {
    "path": "DuDoong-Api/src/test/java/band/gosrock/api/supports/ApiIntegrateTestConfig.java",
    "content": "package band.gosrock.api.supports;\n\n\nimport band.gosrock.DuDoongApiServerApplication;\nimport band.gosrock.common.DuDoongCommonApplication;\nimport band.gosrock.domain.DuDoongDomainApplication;\nimport band.gosrock.infrastructure.DuDoongInfraApplication;\nimport org.springframework.context.annotation.ComponentScan;\nimport org.springframework.context.annotation.Configuration;\n\n/** 스프링 부트 설정의 컴포넌트 스캔범위를 지정 통합 테스트를 위함 */\n@Configuration\n@ComponentScan(\n        basePackageClasses = {\n            DuDoongInfraApplication.class,\n            DuDoongDomainApplication.class,\n            DuDoongCommonApplication.class,\n            DuDoongApiServerApplication.class\n        })\npublic class ApiIntegrateTestConfig {}\n"
  },
  {
    "path": "DuDoong-Api/src/test/kotlin/band/gosrock/api/auth/service/helper/CookieHelperTest.kt",
    "content": "package band.gosrock.api.auth.service.helper\n\nimport band.gosrock.api.auth.model.dto.response.TokenAndUserResponse\nimport band.gosrock.common.helper.SpringEnvironmentHelper\nimport jakarta.servlet.http.Cookie\nimport jakarta.servlet.http.HttpServletRequest\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Assertions.assertNull\nimport org.junit.jupiter.api.Assertions.assertTrue\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.DisplayName\nimport org.junit.jupiter.api.Nested\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.extension.ExtendWith\nimport org.mockito.Mock\nimport org.mockito.Mockito.`when`\nimport org.mockito.Mockito.mock\nimport org.mockito.junit.jupiter.MockitoExtension\nimport org.springframework.http.HttpHeaders\n\n@ExtendWith(MockitoExtension::class)\n@DisplayName(\"CookieHelper\")\nclass CookieHelperTest {\n\n    @Mock\n    private lateinit var springEnvironmentHelper: SpringEnvironmentHelper\n\n    @Mock\n    private lateinit var request: HttpServletRequest\n\n    private lateinit var cookieHelper: CookieHelper\n\n    private val tokenResponse = TokenAndUserResponse(\n        accessToken = \"access-token-value\",\n        accessTokenAge = 3600L,\n        refreshToken = \"refresh-token-value\",\n        refreshTokenAge = 86400L,\n        userProfile = mock(band.gosrock.domain.common.dto.ProfileViewDto::class.java)\n    )\n\n    @BeforeEach\n    fun setUp() {\n        cookieHelper = CookieHelper(springEnvironmentHelper)\n    }\n\n    // ---------------------------------------------------------------------------\n    // Helpers\n    // ---------------------------------------------------------------------------\n\n    private fun setCookiesOnRequest(vararg cookies: Cookie) {\n        `when`(request.cookies).thenReturn(cookies)\n    }\n\n    /** Parse raw Set-Cookie string into a map of attribute -> value (value is \"\" for flags). */\n    private fun parseCookieHeader(header: String): Map<String, String> {\n        return header.split(\";\")\n            .map { it.trim() }\n            .associate { part ->\n                val idx = part.indexOf('=')\n                if (idx == -1) part to \"\" else part.substring(0, idx) to part.substring(idx + 1)\n            }\n    }\n\n    private fun extractSetCookieHeaders(headers: HttpHeaders): List<Map<String, String>> =\n        (headers[HttpHeaders.SET_COOKIE] ?: emptyList()).map { parseCookieHeader(it) }\n\n    // ---------------------------------------------------------------------------\n    // Cookie Name Tests\n    // ---------------------------------------------------------------------------\n\n    @Nested\n    @DisplayName(\"쿠키 이름 (항상 고정)\")\n    inner class CookieNames {\n\n        @Test\n        @DisplayName(\"어떤 프로파일에서도 accessToken 이름은 accessToken\")\n        fun `accessToken name is always accessToken`() {\n            assertEquals(\"accessToken\", cookieHelper.getAccessTokenName())\n        }\n\n        @Test\n        @DisplayName(\"어떤 프로파일에서도 refreshToken 이름은 refreshToken\")\n        fun `refreshToken name is always refreshToken`() {\n            assertEquals(\"refreshToken\", cookieHelper.getRefreshTokenName())\n        }\n    }\n\n    // ---------------------------------------------------------------------------\n    // Domain Attribute Tests\n    // ---------------------------------------------------------------------------\n\n    @Nested\n    @DisplayName(\"도메인 설정 (프로파일별)\")\n    inner class DomainAttribute {\n\n        @Test\n        @DisplayName(\"prod 프로파일: Set-Cookie에 domain=.dudoong.com 포함\")\n        fun `prod profile sets domain to dudoong com`() {\n            `when`(springEnvironmentHelper.isProdProfile()).thenReturn(true)\n            `when`(springEnvironmentHelper.isProdAndStagingProfile()).thenReturn(true)\n\n            val headers = cookieHelper.getTokenCookies(tokenResponse)\n            val cookies = extractSetCookieHeaders(headers)\n\n            cookies.forEach { attrs ->\n                assertEquals(\".dudoong.com\", attrs[\"Domain\"],\n                    \"prod 프로파일에서 Domain은 .dudoong.com 이어야 합니다\")\n            }\n        }\n\n        @Test\n        @DisplayName(\"staging 프로파일: Set-Cookie에 domain=.dudoong.com 포함\")\n        fun `staging profile sets domain to dudoong com`() {\n            `when`(springEnvironmentHelper.isProdProfile()).thenReturn(false)\n            `when`(springEnvironmentHelper.isProdAndStagingProfile()).thenReturn(true)\n\n            val headers = cookieHelper.getTokenCookies(tokenResponse)\n            val cookies = extractSetCookieHeaders(headers)\n\n            cookies.forEach { attrs ->\n                assertEquals(\".dudoong.com\", attrs[\"Domain\"],\n                    \"staging 프로파일에서 Domain은 .dudoong.com 이어야 합니다\")\n            }\n        }\n\n        @Test\n        @DisplayName(\"local 프로파일: Set-Cookie에 domain 속성 없음\")\n        fun `local profile does not set domain`() {\n            `when`(springEnvironmentHelper.isProdProfile()).thenReturn(false)\n            `when`(springEnvironmentHelper.isProdAndStagingProfile()).thenReturn(false)\n\n            val headers = cookieHelper.getTokenCookies(tokenResponse)\n            val cookies = extractSetCookieHeaders(headers)\n\n            cookies.forEach { attrs ->\n                assertTrue(!attrs.containsKey(\"Domain\"),\n                    \"local 프로파일에서는 Domain 속성이 없어야 합니다\")\n            }\n        }\n    }\n\n    // ---------------------------------------------------------------------------\n    // SameSite Tests\n    // ---------------------------------------------------------------------------\n\n    @Nested\n    @DisplayName(\"SameSite 설정 (프로파일별)\")\n    inner class SameSiteAttribute {\n\n        @Test\n        @DisplayName(\"prod 프로파일: SameSite=Strict\")\n        fun `prod profile uses SameSite Strict`() {\n            `when`(springEnvironmentHelper.isProdProfile()).thenReturn(true)\n            `when`(springEnvironmentHelper.isProdAndStagingProfile()).thenReturn(true)\n\n            val headers = cookieHelper.getTokenCookies(tokenResponse)\n            val cookies = extractSetCookieHeaders(headers)\n\n            cookies.forEach { attrs ->\n                assertEquals(\"Strict\", attrs[\"SameSite\"],\n                    \"prod 프로파일에서 SameSite는 Strict여야 합니다\")\n            }\n        }\n\n        @Test\n        @DisplayName(\"staging 프로파일: SameSite=None\")\n        fun `staging profile uses SameSite None`() {\n            `when`(springEnvironmentHelper.isProdProfile()).thenReturn(false)\n            `when`(springEnvironmentHelper.isProdAndStagingProfile()).thenReturn(true)\n\n            val headers = cookieHelper.getTokenCookies(tokenResponse)\n            val cookies = extractSetCookieHeaders(headers)\n\n            cookies.forEach { attrs ->\n                assertEquals(\"None\", attrs[\"SameSite\"],\n                    \"staging 프로파일에서 SameSite는 None이어야 합니다\")\n            }\n        }\n\n        @Test\n        @DisplayName(\"local 프로파일: SameSite=None\")\n        fun `local profile uses SameSite None`() {\n            `when`(springEnvironmentHelper.isProdProfile()).thenReturn(false)\n            `when`(springEnvironmentHelper.isProdAndStagingProfile()).thenReturn(false)\n\n            val headers = cookieHelper.getTokenCookies(tokenResponse)\n            val cookies = extractSetCookieHeaders(headers)\n\n            cookies.forEach { attrs ->\n                assertEquals(\"None\", attrs[\"SameSite\"],\n                    \"local 프로파일에서 SameSite는 None이어야 합니다\")\n            }\n        }\n    }\n\n    // ---------------------------------------------------------------------------\n    // Secure Flag Tests\n    // ---------------------------------------------------------------------------\n\n    @Nested\n    @DisplayName(\"Secure 플래그\")\n    inner class SecureFlag {\n\n        @Test\n        @DisplayName(\"모든 프로파일에서 Secure 플래그가 설정된다\")\n        fun `all profiles always set Secure flag`() {\n            `when`(springEnvironmentHelper.isProdProfile()).thenReturn(false)\n            `when`(springEnvironmentHelper.isProdAndStagingProfile()).thenReturn(false)\n\n            val headers = cookieHelper.getTokenCookies(tokenResponse)\n            val cookies = extractSetCookieHeaders(headers)\n\n            cookies.forEach { attrs ->\n                assertTrue(attrs.containsKey(\"Secure\"),\n                    \"모든 프로파일에서 Secure 플래그가 있어야 합니다\")\n            }\n        }\n    }\n\n    // ---------------------------------------------------------------------------\n    // getTokenCookies - cookie count, names, values, maxAge, path\n    // ---------------------------------------------------------------------------\n\n    @Nested\n    @DisplayName(\"getTokenCookies 쿠키 구성\")\n    inner class GetTokenCookies {\n\n        @Test\n        @DisplayName(\"응답에 accessToken과 refreshToken 두 개의 Set-Cookie 헤더가 포함된다\")\n        fun `returns two Set-Cookie headers`() {\n            `when`(springEnvironmentHelper.isProdProfile()).thenReturn(false)\n            `when`(springEnvironmentHelper.isProdAndStagingProfile()).thenReturn(false)\n\n            val headers = cookieHelper.getTokenCookies(tokenResponse)\n            val setCookies = headers[HttpHeaders.SET_COOKIE] ?: emptyList()\n\n            assertEquals(2, setCookies.size, \"Set-Cookie 헤더가 정확히 2개여야 합니다\")\n        }\n\n        @Test\n        @DisplayName(\"쿠키 이름은 항상 accessToken과 refreshToken (stg_ 접두사 없음)\")\n        fun `cookie names are always accessToken and refreshToken`() {\n            `when`(springEnvironmentHelper.isProdProfile()).thenReturn(false)\n            `when`(springEnvironmentHelper.isProdAndStagingProfile()).thenReturn(false)\n\n            val headers = cookieHelper.getTokenCookies(tokenResponse)\n            val cookieNames = extractSetCookieHeaders(headers).map { it.keys.first() }\n\n            assertTrue(cookieNames.contains(\"accessToken\"))\n            assertTrue(cookieNames.contains(\"refreshToken\"))\n        }\n\n        @Test\n        @DisplayName(\"쿠키에 올바른 토큰 값이 설정된다\")\n        fun `cookie values match token values`() {\n            `when`(springEnvironmentHelper.isProdProfile()).thenReturn(false)\n            `when`(springEnvironmentHelper.isProdAndStagingProfile()).thenReturn(false)\n\n            val headers = cookieHelper.getTokenCookies(tokenResponse)\n            val cookies = extractSetCookieHeaders(headers)\n            val valueMap = cookies.associate { it.keys.first() to it.values.first() }\n\n            assertEquals(\"access-token-value\", valueMap[\"accessToken\"])\n            assertEquals(\"refresh-token-value\", valueMap[\"refreshToken\"])\n        }\n\n        @Test\n        @DisplayName(\"쿠키에 maxAge가 TokenAndUserResponse의 값으로 설정된다\")\n        fun `cookie maxAge is set from token response`() {\n            `when`(springEnvironmentHelper.isProdProfile()).thenReturn(false)\n            `when`(springEnvironmentHelper.isProdAndStagingProfile()).thenReturn(false)\n\n            val headers = cookieHelper.getTokenCookies(tokenResponse)\n            val cookies = extractSetCookieHeaders(headers)\n\n            val accessAttrs = cookies.first { it.keys.first() == \"accessToken\" }\n            val refreshAttrs = cookies.first { it.keys.first() == \"refreshToken\" }\n\n            assertEquals(\"3600\", accessAttrs[\"Max-Age\"])\n            assertEquals(\"86400\", refreshAttrs[\"Max-Age\"])\n        }\n\n        @Test\n        @DisplayName(\"쿠키 path는 /로 설정된다\")\n        fun `cookie path is root`() {\n            `when`(springEnvironmentHelper.isProdProfile()).thenReturn(false)\n            `when`(springEnvironmentHelper.isProdAndStagingProfile()).thenReturn(false)\n\n            val headers = cookieHelper.getTokenCookies(tokenResponse)\n            val cookies = extractSetCookieHeaders(headers)\n\n            cookies.forEach { attrs ->\n                assertEquals(\"/\", attrs[\"Path\"], \"쿠키 Path는 /여야 합니다\")\n            }\n        }\n    }\n\n    // ---------------------------------------------------------------------------\n    // deleteCookies\n    // ---------------------------------------------------------------------------\n\n    @Nested\n    @DisplayName(\"deleteCookies\")\n    inner class DeleteCookies {\n\n        @Test\n        @DisplayName(\"두 개의 Set-Cookie 헤더가 반환된다\")\n        fun `returns two Set-Cookie headers`() {\n            `when`(springEnvironmentHelper.isProdProfile()).thenReturn(false)\n            `when`(springEnvironmentHelper.isProdAndStagingProfile()).thenReturn(false)\n\n            val headers = cookieHelper.deleteCookies()\n            val setCookies = headers[HttpHeaders.SET_COOKIE] ?: emptyList()\n\n            assertEquals(2, setCookies.size)\n        }\n\n        @Test\n        @DisplayName(\"쿠키 이름은 항상 accessToken, refreshToken (stg_ 접두사 없음)\")\n        fun `delete cookie names are always accessToken and refreshToken`() {\n            `when`(springEnvironmentHelper.isProdProfile()).thenReturn(false)\n            `when`(springEnvironmentHelper.isProdAndStagingProfile()).thenReturn(false)\n\n            val headers = cookieHelper.deleteCookies()\n            val cookieNames = extractSetCookieHeaders(headers).map { it.keys.first() }\n\n            assertTrue(cookieNames.contains(\"accessToken\"))\n            assertTrue(cookieNames.contains(\"refreshToken\"))\n        }\n\n        @Test\n        @DisplayName(\"쿠키 값이 빈 문자열로 설정된다\")\n        fun `cookie values are empty string`() {\n            `when`(springEnvironmentHelper.isProdProfile()).thenReturn(false)\n            `when`(springEnvironmentHelper.isProdAndStagingProfile()).thenReturn(false)\n\n            val headers = cookieHelper.deleteCookies()\n            val cookies = extractSetCookieHeaders(headers)\n            val valueMap = cookies.associate { it.keys.first() to it.values.first() }\n\n            assertEquals(\"\", valueMap[\"accessToken\"])\n            assertEquals(\"\", valueMap[\"refreshToken\"])\n        }\n\n        @Test\n        @DisplayName(\"maxAge=0으로 설정되어 쿠키가 즉시 만료된다\")\n        fun `maxAge is zero for immediate expiry`() {\n            `when`(springEnvironmentHelper.isProdProfile()).thenReturn(false)\n            `when`(springEnvironmentHelper.isProdAndStagingProfile()).thenReturn(false)\n\n            val headers = cookieHelper.deleteCookies()\n            val cookies = extractSetCookieHeaders(headers)\n\n            cookies.forEach { attrs ->\n                assertEquals(\"0\", attrs[\"Max-Age\"], \"삭제 쿠키의 Max-Age는 0이어야 합니다\")\n            }\n        }\n\n        @Test\n        @DisplayName(\"prod 프로파일: 삭제 쿠키에도 domain=.dudoong.com 포함\")\n        fun `prod delete cookies include domain`() {\n            `when`(springEnvironmentHelper.isProdProfile()).thenReturn(true)\n            `when`(springEnvironmentHelper.isProdAndStagingProfile()).thenReturn(true)\n\n            val headers = cookieHelper.deleteCookies()\n            val cookies = extractSetCookieHeaders(headers)\n\n            cookies.forEach { attrs ->\n                assertEquals(\".dudoong.com\", attrs[\"Domain\"],\n                    \"prod 삭제 쿠키에도 Domain이 .dudoong.com이어야 합니다\")\n            }\n        }\n    }\n\n    // ---------------------------------------------------------------------------\n    // getRefreshTokenFromRequest\n    // ---------------------------------------------------------------------------\n\n    @Nested\n    @DisplayName(\"getRefreshTokenFromRequest\")\n    inner class GetRefreshTokenFromRequest {\n\n        @Test\n        @DisplayName(\"refreshToken 이름으로 쿠키를 읽는다\")\n        fun `reads refreshToken cookie by name`() {\n            setCookiesOnRequest(Cookie(\"refreshToken\", \"my-refresh-value\"))\n\n            val result = cookieHelper.getRefreshTokenFromRequest(request)\n\n            assertEquals(\"my-refresh-value\", result)\n        }\n\n        @Test\n        @DisplayName(\"요청에 쿠키가 없으면 null을 반환한다\")\n        fun `returns null when request has no cookies`() {\n            `when`(request.cookies).thenReturn(null)\n\n            val result = cookieHelper.getRefreshTokenFromRequest(request)\n\n            assertNull(result, \"쿠키가 없을 때 null을 반환해야 합니다\")\n        }\n\n        @Test\n        @DisplayName(\"요청에 다른 쿠키만 있을 때 null을 반환한다\")\n        fun `returns null when matching cookie is absent`() {\n            setCookiesOnRequest(\n                Cookie(\"someOtherCookie\", \"value1\"),\n                Cookie(\"anotherCookie\", \"value2\")\n            )\n\n            val result = cookieHelper.getRefreshTokenFromRequest(request)\n\n            assertNull(result, \"refreshToken 쿠키가 없을 때 null을 반환해야 합니다\")\n        }\n\n        @Test\n        @DisplayName(\"여러 쿠키 중 올바른 refreshToken 값을 반환한다\")\n        fun `returns correct refreshToken value among multiple cookies`() {\n            setCookiesOnRequest(\n                Cookie(\"accessToken\", \"access-value\"),\n                Cookie(\"refreshToken\", \"correct-refresh\"),\n                Cookie(\"unrelated\", \"other\")\n            )\n\n            val result = cookieHelper.getRefreshTokenFromRequest(request)\n\n            assertEquals(\"correct-refresh\", result)\n        }\n    }\n\n    // ---------------------------------------------------------------------------\n    // Cross-cutting: HttpOnly is NOT set (ResponseCookie default)\n    // ---------------------------------------------------------------------------\n\n    @Nested\n    @DisplayName(\"HttpOnly 플래그 (보안 검증)\")\n    inner class HttpOnlyFlag {\n\n        @Test\n        @DisplayName(\"쿠키에 HttpOnly 플래그가 없다 (JS 접근 가능 — 현재 구현 확인)\")\n        fun `cookies do not have HttpOnly flag`() {\n            `when`(springEnvironmentHelper.isProdProfile()).thenReturn(true)\n            `when`(springEnvironmentHelper.isProdAndStagingProfile()).thenReturn(true)\n\n            val headers = cookieHelper.getTokenCookies(tokenResponse)\n            val rawHeaders = headers[HttpHeaders.SET_COOKIE] ?: emptyList()\n\n            rawHeaders.forEach { header ->\n                val parts = header.split(\";\").map { it.trim().lowercase() }\n                assertTrue(!parts.contains(\"httponly\"),\n                    \"현재 구현에서 HttpOnly 플래그는 없어야 합니다 (의도적 설계 확인용 테스트)\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/test/kotlin/band/gosrock/api/common/aop/hostRole/HostRoleAopTest.kt",
    "content": "package band.gosrock.api.common.aop.hostRole\n\nimport org.aspectj.lang.ProceedingJoinPoint\nimport org.aspectj.lang.reflect.MethodSignature\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Assertions.assertThrows\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.DisplayName\nimport org.junit.jupiter.api.Nested\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.extension.ExtendWith\nimport org.mockito.Mock\nimport org.mockito.Mockito.`when`\nimport org.mockito.junit.jupiter.MockitoExtension\n\n@ExtendWith(MockitoExtension::class)\n@DisplayName(\"HostRoleAop\")\nclass HostRoleAopTest {\n\n    private lateinit var hostRoleAop: HostRoleAop\n\n    @Mock\n    private lateinit var hostCallTransactionFactory: HostCallTransactionFactory\n\n    @BeforeEach\n    fun setUp() {\n        hostRoleAop = HostRoleAop(hostCallTransactionFactory)\n    }\n\n    @Nested\n    @DisplayName(\"getId\")\n    inner class GetIdTest {\n\n        @Test\n        @DisplayName(\"파라미터 이름으로 userId를 찾아 반환한다\")\n        fun getUserIdSuccess() {\n            val parameterNames = arrayOf(\"userId\", \"eventId\")\n            val args = arrayOf<Any?>(1L, 2L)\n\n            val result = hostRoleAop.getId(parameterNames, args, \"userId\")\n\n            assertEquals(1L, result)\n        }\n\n        @Test\n        @DisplayName(\"파라미터 이름으로 eventId를 찾아 반환한다\")\n        fun getEventIdSuccess() {\n            val parameterNames = arrayOf(\"userId\", \"eventId\", \"request\")\n            val args = arrayOf<Any?>(1L, 2L, \"dummy\")\n\n            val result = hostRoleAop.getId(parameterNames, args, \"eventId\")\n\n            assertEquals(2L, result)\n        }\n\n        @Test\n        @DisplayName(\"userId 파라미터가 없으면 IllegalArgumentException 발생\")\n        fun noUserIdThrowsException() {\n            val parameterNames = arrayOf(\"eventId\", \"request\")\n            val args = arrayOf<Any?>(2L, \"dummy\")\n\n            assertThrows(IllegalArgumentException::class.java) {\n                hostRoleAop.getId(parameterNames, args, \"userId\")\n            }\n        }\n\n        @Test\n        @DisplayName(\"파라미터 이름이 없는 경우 IllegalArgumentException 발생\")\n        fun emptyParamsThrowsException() {\n            val parameterNames = emptyArray<String>()\n            val args = emptyArray<Any?>()\n\n            assertThrows(IllegalArgumentException::class.java) {\n                hostRoleAop.getId(parameterNames, args, \"userId\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/test/kotlin/band/gosrock/api/refund/service/CompleteRefundUseCaseTest.kt",
    "content": "package band.gosrock.api.refund.service\n\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderMethod\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport band.gosrock.domain.domains.order.domain.RefundStatus\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Assertions.assertNotNull\nimport org.junit.jupiter.api.DisplayName\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.extension.ExtendWith\nimport org.mockito.BDDMockito.given\nimport org.mockito.InjectMocks\nimport org.mockito.Mock\nimport org.mockito.junit.jupiter.MockitoExtension\n\n@ExtendWith(MockitoExtension::class)\n@DisplayName(\"CompleteRefundUseCase\")\nclass CompleteRefundUseCaseTest {\n\n    @Mock\n    private lateinit var orderAdaptor: OrderAdaptor\n\n    @Mock\n    private lateinit var userAdaptor: UserAdaptor\n\n    @Mock\n    private lateinit var eventAdaptor: EventAdaptor\n\n    @InjectMocks\n    private lateinit var completeRefundUseCase: CompleteRefundUseCase\n\n    @Test\n    @DisplayName(\"환불 확인 시 refundStatus가 REFUND_COMPLETED로 변경된다\")\n    fun completeRefund() {\n        // given\n        val order = Order.forTest(\n            userId = 1L,\n            orderName = \"테스트주문\",\n            orderStatus = OrderStatus.CANCELED,\n            orderMethod = OrderMethod.PAYMENT,\n            eventId = 100L,\n            cancelReason = \"단순 변심\",\n            refundStatus = RefundStatus.REFUND_REQUESTED,\n        )\n        given(orderAdaptor.findByOrderUuid(\"test-uuid\")).willReturn(order)\n\n        // when\n        val response = completeRefundUseCase.execute(1L, 100L, \"test-uuid\")\n\n        // then\n        assertEquals(RefundStatus.REFUND_COMPLETED, response.refundStatus)\n        assertEquals(RefundStatus.REFUND_COMPLETED, order.refundStatus)\n        assertNotNull(order.refundStatusChangedAt)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/test/kotlin/band/gosrock/api/statistic/query/result/IssuedTicketStatisticTest.kt",
    "content": "package band.gosrock.api.statistic.query.result\n\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Test\n\nclass IssuedTicketStatisticTest {\n\n    @Test\n    fun `발급 0건 입장 0건이면 미입장도 0건이다`() {\n        val statistic = IssuedTicketStatistic(0L, 0L)\n\n        assertEquals(0L, statistic.issuedCount)\n        assertEquals(0L, statistic.enteredCount)\n        assertEquals(0L, statistic.notEnteredCount)\n    }\n\n    @Test\n    fun `발급 10건 입장 3건이면 미입장 7건이다`() {\n        val statistic = IssuedTicketStatistic(10L, 3L)\n\n        assertEquals(10L, statistic.issuedCount)\n        assertEquals(3L, statistic.enteredCount)\n        assertEquals(7L, statistic.notEnteredCount)\n    }\n\n    @Test\n    fun `전원 입장하면 미입장 0건이다`() {\n        val statistic = IssuedTicketStatistic(5L, 5L)\n\n        assertEquals(5L, statistic.issuedCount)\n        assertEquals(5L, statistic.enteredCount)\n        assertEquals(0L, statistic.notEnteredCount)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/test/kotlin/band/gosrock/api/statistic/query/result/OrderStatisticTest.kt",
    "content": "package band.gosrock.api.statistic.query.result\n\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Test\nimport java.math.BigDecimal\n\nclass OrderStatisticTest {\n\n    @Test\n    fun `sellAmount가 null이면 0원으로 처리된다`() {\n        val statistic = OrderStatistic(0L, 0L, null)\n\n        assertEquals(0L, statistic.DoneCount)\n        assertEquals(0L, statistic.notApprovedCount)\n        assertEquals(0, statistic.sellAmount.amount.compareTo(BigDecimal.ZERO))\n    }\n\n    @Test\n    fun `sellAmount가 정상값이면 Money로 변환된다`() {\n        val statistic = OrderStatistic(5L, 2L, BigDecimal.valueOf(50000))\n\n        assertEquals(5L, statistic.DoneCount)\n        assertEquals(2L, statistic.notApprovedCount)\n        assertEquals(50000L, statistic.sellAmount.longValue())\n    }\n\n    @Test\n    fun `모든 값이 0이면 정상 생성된다`() {\n        val statistic = OrderStatistic(0L, 0L, BigDecimal.ZERO)\n\n        assertEquals(0L, statistic.DoneCount)\n        assertEquals(0L, statistic.notApprovedCount)\n        assertEquals(0L, statistic.sellAmount.longValue())\n    }\n}\n"
  },
  {
    "path": "DuDoong-Api/src/test/resources/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n  <include resource=\"org/springframework/boot/logging/logback/base.xml\" />\n  <logger name=\"org.springframework\" level=\"INFO\"/>\n  <include resource=\"band.gosrock\"/>\n  <logger name=\"band.gosrock\" level=\"DEBUG\"/>\n</configuration>"
  },
  {
    "path": "DuDoong-Batch/Dockerfile",
    "content": "FROM eclipse-temurin:21-jre-alpine\n\nEXPOSE 8080\n\nCOPY ./build/libs/*.jar app.jar\nARG PROFILE=dev\nENV PROFILE=${PROFILE}\n\nENTRYPOINT [\"java\",\"-Dspring.profiles.active=${PROFILE}\", \"-Djava.security.egd=file:/dev/./urandom\",\"-jar\",\"-Duser.timezone=Asia/Seoul\",\"/app.jar\"]"
  },
  {
    "path": "DuDoong-Batch/build.gradle.kts",
    "content": "dependencies {\n    implementation(\"org.springframework.boot:spring-boot-starter-batch\")\n    implementation(project(\":DuDoong-Domain\"))\n    implementation(project(\":DuDoong-Common\"))\n    implementation(project(\":DuDoong-Infrastructure\"))\n    testImplementation(\"org.springframework.batch:spring-batch-test\")\n    implementation(\"org.apache.poi:poi:5.2.0\")\n    implementation(\"org.apache.poi:poi-ooxml:5.2.0\")\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/BatchApplication.kt",
    "content": "package band.gosrock\n\nimport org.springframework.batch.core.configuration.annotation.EnableBatchProcessing\nimport org.springframework.boot.SpringApplication\nimport org.springframework.boot.autoconfigure.SpringBootApplication\n\n@EnableBatchProcessing\n@SpringBootApplication\nclass BatchApplication\n\nfun main(args: Array<String>) {\n    val context = SpringApplication.run(BatchApplication::class.java, *args)\n    System.exit(SpringApplication.exit(context))\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/dto/SettlementPDFDto.kt",
    "content": "package band.gosrock.dto\n\nimport java.time.LocalDateTime\n\ndata class SettlementPDFDto(\n    val eventTitle: String,\n    val hostName: String,\n    val settlementAt: LocalDateTime,\n    val dudoongTicketAmount: String,\n    val pgTicketAmount: String,\n    val totalAmount: String,\n    val dudoongFee: String,\n    val pgFee: String,\n    val totalFee: String,\n    val totalFeeVat: String,\n    val totalSettlement: String,\n    val now: LocalDateTime,\n)\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/helper/SettlementEmailHelper.kt",
    "content": "package band.gosrock.helper\n\nimport band.gosrock.common.annotation.Helper\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.infrastructure.config.s3.S3PrivateFileService\nimport band.gosrock.infrastructure.config.ses.AwsSesUtils\nimport band.gosrock.infrastructure.config.ses.RawEmailAttachmentDto\nimport band.gosrock.infrastructure.config.ses.SendRawEmailDto\nimport jakarta.mail.MessagingException\nimport org.thymeleaf.context.Context\nimport org.thymeleaf.spring6.SpringTemplateEngine\n\n@Helper\nclass SettlementEmailHelper(\n    private val templateEngine: SpringTemplateEngine,\n    private val s3PrivateFileService: S3PrivateFileService,\n    private val awsSesUtils: AwsSesUtils,\n) {\n\n    private fun getSettlementPdfAttachment(event: Event): RawEmailAttachmentDto =\n        RawEmailAttachmentDto(\n            fileName = event.getEventName() + \"_정산서.pdf\",\n            fileBytes = s3PrivateFileService.downloadEventSettlementPdf(event.id!!),\n            type = \"application/pdf\",\n        )\n\n    private fun getOrderListExcelAttachment(event: Event): RawEmailAttachmentDto =\n        RawEmailAttachmentDto(\n            fileName = event.getEventName() + \"_주문목록.xlsx\",\n            fileBytes = s3PrivateFileService.downloadEventOrdersExcel(event.id!!),\n            type = \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n        )\n\n    @Throws(MessagingException::class)\n    fun sendToAdmin(event: Event) {\n        val sendRawEmailDto = SendRawEmailDto(\n            bodyHtml = templateEngine.process(\"eventSettlement\", Context()),\n            recipient = \"support@dudoong.com\",\n            subject = event.getEventName() + \"공연 정산서 어드민 발송 ( 관리자용 )\",\n        )\n\n        sendRawEmailDto.addEmailAttachments(getSettlementPdfAttachment(event))\n        sendRawEmailDto.addEmailAttachments(getOrderListExcelAttachment(event))\n        awsSesUtils.sendRawEmails(sendRawEmailDto)\n    }\n\n    @Throws(MessagingException::class)\n    fun sendToHost(event: Event, hostUserEmail: String) {\n        val sendRawEmailDto = SendRawEmailDto(\n            bodyHtml = templateEngine.process(\"eventSettlement\", Context()),\n            recipient = hostUserEmail,\n            subject = event.getEventName() + \"공연 정산관련 안내\",\n        )\n\n        sendRawEmailDto.addEmailAttachments(getSettlementPdfAttachment(event))\n        sendRawEmailDto.addEmailAttachments(getOrderListExcelAttachment(event))\n        awsSesUtils.sendRawEmails(sendRawEmailDto)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/helper/SettlementPdfHelper.kt",
    "content": "package band.gosrock.helper\n\nimport band.gosrock.common.annotation.Helper\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.settlement.domain.EventSettlement\nimport band.gosrock.domain.domains.user.domain.User\nimport band.gosrock.dto.SettlementPDFDto\nimport band.gosrock.infrastructure.config.pdf.PdfRender\nimport band.gosrock.infrastructure.config.s3.S3PrivateFileService\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.lowagie.text.DocumentException\nimport java.io.IOException\nimport java.time.LocalDateTime\nimport org.thymeleaf.context.Context\nimport org.thymeleaf.spring6.SpringTemplateEngine\n\n@Helper\nclass SettlementPdfHelper(\n    private val pdfRender: PdfRender,\n    private val objectMapper: ObjectMapper,\n    private val templateEngine: SpringTemplateEngine,\n    private val s3PrivateFileUploadService: S3PrivateFileService,\n) {\n\n    @Throws(DocumentException::class, IOException::class)\n    fun uploadPdfToS3(event: Event, eventSettlement: EventSettlement, masterUser: User) {\n        val settlementPDFDto = getSettlementPDFDto(event, masterUser, eventSettlement)\n        // 정산 관련 타임리프 파일.\n        val html = templateEngine.process(\"settlement\", getPdfHtmlContext(settlementPDFDto))\n        val outputStream = pdfRender.generatePdfFromHtml(html)\n        s3PrivateFileUploadService.eventSettlementPdfUpload(event.id!!, outputStream)\n    }\n\n    private fun getPdfHtmlContext(settlementPDFDto: SettlementPDFDto): Context {\n        @Suppress(\"UNCHECKED_CAST\")\n        val result = objectMapper.convertValue(settlementPDFDto, Map::class.java) as Map<String, Any>\n        val context = Context(null, result)\n        context.setVariable(\"settlementAt\", settlementPDFDto.settlementAt)\n        context.setVariable(\"now\", settlementPDFDto.now)\n        return context\n    }\n\n    private fun getSettlementPDFDto(\n        event: Event,\n        masterUser: User,\n        eventSettlement: EventSettlement,\n    ): SettlementPDFDto =\n        SettlementPDFDto(\n            eventTitle = event.eventBasic!!.name!!,\n            hostName = masterUser.profile!!.name!!,\n            settlementAt = event.getEndAt()!!.plusDays(6L),\n            dudoongTicketAmount = eventSettlement.dudoongAmount.toString(),\n            pgTicketAmount = eventSettlement.paymentAmount.toString(),\n            totalAmount = eventSettlement.totalSalesAmount.toString(),\n            // 초기 두둥 자체 수수료 없음.\n            dudoongFee = Money.ZERO.toString(),\n            pgFee = eventSettlement.pgFee.toString(),\n            totalFee = eventSettlement.pgFee.toString(),\n            totalFeeVat = eventSettlement.pgFeeVat.toString(),\n            totalSettlement = eventSettlement.totalAmount.toString(),\n            now = LocalDateTime.now(),\n        )\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/helper/excel/ExcelOrderDto.kt",
    "content": "package band.gosrock.helper.excel\n\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderMethod\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport java.time.LocalDateTime\n\ndata class ExcelOrderDto(\n    val orderNo: String,\n    val orderMethod: OrderMethod,\n    val orderStatus: OrderStatus,\n    val orderName: String,\n    val userId: Long,\n    val amount: Money,\n    val quantity: Long,\n    val createdAt: LocalDateTime,\n    val refundAt: LocalDateTime?,\n) {\n    companion object {\n        fun from(order: Order): ExcelOrderDto =\n            ExcelOrderDto(\n                amount = order.getTotalPaymentPrice(),\n                orderNo = order.orderNo!!,\n                orderMethod = order.orderMethod!!,\n                orderStatus = order.orderStatus,\n                orderName = order.orderName!!,\n                userId = order.userId!!,\n                quantity = order.getTotalQuantity(),\n                createdAt = order.createdAt!!,\n                refundAt = order.withDrawAt,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/helper/excel/ExcelOrderHelper.kt",
    "content": "package band.gosrock.helper.excel\n\nimport java.io.ByteArrayOutputStream\nimport java.time.format.DateTimeFormatter\nimport org.apache.poi.ss.usermodel.FillPatternType\nimport org.apache.poi.ss.usermodel.IndexedColors\nimport org.apache.poi.xssf.usermodel.XSSFWorkbook\nimport org.springframework.stereotype.Component\n\n@Component\nclass ExcelOrderHelper {\n\n    private val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm\")\n\n    fun execute(excelOrders: List<ExcelOrderDto>): ByteArrayOutputStream {\n        try {\n            val workbook = XSSFWorkbook()\n            val sheet = workbook.createSheet(\"orderList\")\n            for (i in 0..8) {\n                sheet.setColumnWidth(i, 6000)\n            }\n\n            // create header\n            val header = sheet.createRow(0)\n            val headerStyle = workbook.createCellStyle()\n            headerStyle.fillForegroundColor = IndexedColors.LIGHT_GREEN.index\n            headerStyle.fillPattern = FillPatternType.SOLID_FOREGROUND\n            val font = workbook.createFont()\n            font.fontName = \"Arial\"\n            font.fontHeightInPoints = 16\n            font.bold = true\n            headerStyle.setFont(font)\n\n            val headers = listOf(\n                \"주문번호\", \"주문 방식\", \"주문 상태\", \"주문 이름\",\n                \"주문자 아이디\", \"총액\", \"티켓 수량\", \"생성 일시\", \"환불 일시\",\n            )\n            headers.forEachIndexed { idx, title ->\n                val cell = header.createCell(idx)\n                cell.setCellValue(title)\n                cell.cellStyle = headerStyle\n            }\n\n            val style = workbook.createCellStyle()\n            style.wrapText = true\n\n            excelOrders.forEachIndexed { idx, excelOrderDto ->\n                val row = sheet.createRow(idx + 1)\n                row.createCell(0).setCellValue(excelOrderDto.orderNo)\n                row.createCell(1).setCellValue(excelOrderDto.orderMethod.kr)\n                row.createCell(2).setCellValue(excelOrderDto.orderStatus.kr)\n                row.createCell(3).setCellValue(excelOrderDto.orderName)\n                row.createCell(4).setCellValue(excelOrderDto.userId.toDouble())\n                row.createCell(5).setCellValue(excelOrderDto.amount.toString())\n                row.createCell(6).setCellValue(excelOrderDto.quantity.toDouble())\n                row.createCell(7).setCellValue(excelOrderDto.createdAt.format(formatter))\n                row.createCell(8).setCellValue(excelOrderDto.refundAt?.format(formatter))\n            }\n\n            val outputStream = ByteArrayOutputStream()\n            workbook.write(outputStream)\n            workbook.close()\n            return outputStream\n        } catch (e: Exception) {\n            throw RuntimeException(e)\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/helper/slack/SlackEventExpirationSender.kt",
    "content": "package band.gosrock.helper.slack\n\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.infrastructure.config.slack.SlackServiceNotificationProvider\nimport com.slack.api.model.block.Blocks\nimport com.slack.api.model.block.Blocks.divider\nimport com.slack.api.model.block.Blocks.section\nimport com.slack.api.model.block.LayoutBlock\nimport com.slack.api.model.block.composition.BlockCompositions.plainText\nimport com.slack.api.model.block.composition.MarkdownTextObject\nimport java.time.LocalDateTime\nimport java.time.format.DateTimeFormatter\nimport org.slf4j.LoggerFactory\nimport org.springframework.stereotype.Component\n\n@Component\nclass SlackEventExpirationSender(\n    private val slackProvider: SlackServiceNotificationProvider,\n) {\n    private val log = LoggerFactory.getLogger(SlackEventExpirationSender::class.java)\n\n    fun execute(time: LocalDateTime, events: List<Event>) {\n        val formatter = DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm\")\n        val layoutBlocks = mutableListOf<LayoutBlock>()\n\n        layoutBlocks.add(\n            Blocks.header { it.text(plainText(\"공연 자동 만료 알림\")) }\n        )\n        layoutBlocks.add(divider())\n\n        val markdownDateTime = MarkdownTextObject.builder().text(\"* 기준 시간:*\\n\" + time.format(formatter)).build()\n        val markdownTotalEvents = MarkdownTextObject.builder().text(\"* 만료 공연 수:*\\n\" + events.size).build()\n        layoutBlocks.add(section { it.fields(listOf(markdownDateTime, markdownTotalEvents)) })\n        layoutBlocks.add(divider())\n\n        val markdownEventIdTitle = MarkdownTextObject.builder().text(\"* 공연 ID:*\\n\").build()\n        val markdownEventNameTitle = MarkdownTextObject.builder().text(\"* 공연 이름:*\\n\").build()\n        layoutBlocks.add(section { it.fields(listOf(markdownEventIdTitle, markdownEventNameTitle)) })\n\n        events.forEach { event ->\n            val markdownEventId = MarkdownTextObject.builder().text(event.id.toString()).build()\n            val markdownEventName = MarkdownTextObject.builder().text(event.eventBasic!!.name!!).build()\n            layoutBlocks.add(section { it.fields(listOf(markdownEventId, markdownEventName)) })\n        }\n\n        slackProvider.sendNotification(layoutBlocks)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/helper/slack/SlackUserNotificationSender.kt",
    "content": "package band.gosrock.helper.slack\n\nimport band.gosrock.infrastructure.config.slack.SlackServiceNotificationProvider\nimport com.slack.api.model.block.Blocks\nimport com.slack.api.model.block.Blocks.divider\nimport com.slack.api.model.block.Blocks.section\nimport com.slack.api.model.block.LayoutBlock\nimport com.slack.api.model.block.composition.BlockCompositions.plainText\nimport com.slack.api.model.block.composition.MarkdownTextObject\nimport java.time.LocalDate\nimport org.slf4j.LoggerFactory\nimport org.springframework.stereotype.Component\n\n@Component\nclass SlackUserNotificationSender(\n    private val slackProvider: SlackServiceNotificationProvider,\n) {\n    private val log = LoggerFactory.getLogger(SlackUserNotificationSender::class.java)\n\n    fun execute(date: LocalDate, todayUserCount: Long, yesterdayCount: Long) {\n        val layoutBlocks = mutableListOf<LayoutBlock>()\n        layoutBlocks.add(\n            Blocks.header { it.text(plainText(\"유저 관련 일일 통계\")) }\n        )\n        layoutBlocks.add(divider())\n\n        val markdownDate = MarkdownTextObject.builder().text(\"* 실행 일:*\\n$date\").build()\n        val markdownTotalUser = MarkdownTextObject.builder().text(\"* 당일 총 유저 수:*\\n$todayUserCount\").build()\n        layoutBlocks.add(section { it.fields(listOf(markdownDate, markdownTotalUser)) })\n\n        layoutBlocks.add(divider())\n\n        val markdownYesterdayCount = MarkdownTextObject.builder().text(\"* 전일 총 유저 수 :*\\n$yesterdayCount\").build()\n        val markdownUserIncrease = MarkdownTextObject.builder()\n            .text(\"* 유저 증감 :*\\n${todayUserCount - yesterdayCount}\")\n            .build()\n        layoutBlocks.add(section { it.fields(listOf(markdownYesterdayCount, markdownUserIncrease)) })\n\n        slackProvider.sendNotification(layoutBlocks)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/job/EventExpiration.kt",
    "content": "package band.gosrock.job\n\nimport band.gosrock.domain.domains.event.service.EventService\nimport band.gosrock.helper.slack.SlackEventExpirationSender\nimport band.gosrock.parameter.DateTimeJobParameter\nimport org.slf4j.LoggerFactory\nimport org.springframework.batch.core.Job\nimport org.springframework.batch.core.Step\nimport org.springframework.batch.core.configuration.annotation.JobBuilderFactory\nimport org.springframework.batch.core.configuration.annotation.JobScope\nimport org.springframework.batch.core.configuration.annotation.StepBuilderFactory\nimport org.springframework.batch.repeat.RepeatStatus\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\n\n@Configuration\nclass EventExpiration(\n    private val jobBuilderFactory: JobBuilderFactory,\n    private val stepBuilderFactory: StepBuilderFactory,\n    private val slackEventExpirationSender: SlackEventExpirationSender,\n    private val eventService: EventService,\n) {\n    private val log = LoggerFactory.getLogger(EventExpiration::class.java)\n\n    companion object {\n        private const val JOB_NAME = \"이벤트_자동만료\"\n        private const val BEAN_PREFIX = \"${JOB_NAME}_\"\n    }\n\n    @Bean(BEAN_PREFIX + \"dateTimeJobParameter\")\n    @JobScope\n    fun dateTimeJobParameter(): DateTimeJobParameter = DateTimeJobParameter()\n\n    @Bean(JOB_NAME)\n    fun eventExpirationJob(): Job =\n        jobBuilderFactory\n            .get(JOB_NAME)\n            .preventRestart()\n            .start(eventExpirationStep())\n            .build()\n\n    @Bean(BEAN_PREFIX + \"step\")\n    @JobScope\n    fun eventExpirationStep(): Step =\n        stepBuilderFactory\n            .get(BEAN_PREFIX + \"step\")\n            .tasklet { _, _ ->\n                log.info(\">>>>> 이벤트 자동 만료 작업 실행\")\n                val time = dateTimeJobParameter().getTime()\n                val events = eventService.closeExpiredEventsEndAtBefore(time)\n                slackEventExpirationSender.execute(time, events)\n                RepeatStatus.FINISHED\n            }\n            .build()\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/job/EventOrdersToExcel.kt",
    "content": "package band.gosrock.job\n\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.helper.excel.ExcelOrderDto\nimport band.gosrock.helper.excel.ExcelOrderHelper\nimport band.gosrock.infrastructure.config.s3.S3PrivateFileService\nimport band.gosrock.parameter.EventJobParameter\nimport org.slf4j.LoggerFactory\nimport org.springframework.batch.core.Job\nimport org.springframework.batch.core.Step\nimport org.springframework.batch.core.configuration.annotation.JobBuilderFactory\nimport org.springframework.batch.core.configuration.annotation.JobScope\nimport org.springframework.batch.core.configuration.annotation.StepBuilderFactory\nimport org.springframework.batch.repeat.RepeatStatus\nimport org.springframework.beans.factory.annotation.Qualifier\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\n\n/** 공연 관련 주문 목록을 엑셀화 하여 프라이빗한 S3에 저장합니다. */\n@Configuration\nclass EventOrdersToExcel(\n    private val jobBuilderFactory: JobBuilderFactory,\n    private val stepBuilderFactory: StepBuilderFactory,\n    private val excelOrderHelper: ExcelOrderHelper,\n    private val eventAdaptor: EventAdaptor,\n    private val orderAdaptor: OrderAdaptor,\n    @Qualifier(BEAN_PREFIX + \"eventJobParameter\")\n    private val eventJobParameter: EventJobParameter,\n    private val s3PrivateFileUploadService: S3PrivateFileService,\n) {\n    private val log = LoggerFactory.getLogger(EventOrdersToExcel::class.java)\n\n    companion object {\n        private const val JOB_NAME = \"이벤트주문목록_엑셀업로드\"\n        const val BEAN_PREFIX = \"${JOB_NAME}_\"\n    }\n\n    @Bean(BEAN_PREFIX + \"eventJobParameter\")\n    @JobScope\n    fun eventJobParameter(): EventJobParameter = EventJobParameter(eventAdaptor)\n\n    @Bean(JOB_NAME)\n    fun slackUserStatisticJob(): Job =\n        jobBuilderFactory.get(JOB_NAME).preventRestart().start(userStatisticStep()).build()\n\n    @Bean(BEAN_PREFIX + \"step\")\n    @JobScope\n    fun userStatisticStep(): Step =\n        stepBuilderFactory\n            .get(BEAN_PREFIX + \"step\")\n            .tasklet { _, _ ->\n                val event = eventJobParameter.getEvent()\n                val eventOrders = orderAdaptor.findByEventId(event.id!!)\n                val excelOrders = getExcelOrders(eventOrders)\n                s3PrivateFileUploadService.eventOrdersExcelUpload(\n                    event.id!!, excelOrderHelper.execute(excelOrders)\n                )\n                RepeatStatus.FINISHED\n            }\n            .build()\n\n    // 주문 상태가 결제 완료, 승인 완료 , 환불 , 취소 인것만 가져오도록 필터링.\n    private fun getExcelOrders(eventOrders: List<Order>) =\n        eventOrders\n            .filter { it.orderStatus.isInEventOrderExcelStatus() }\n            .map { ExcelOrderDto.from(it) }\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/job/EventSettlementAlimTalkToHost.kt",
    "content": "package band.gosrock.job\n\nimport band.gosrock.domain.common.alarm.SettlementKakaoTalkAlarm\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.infrastructure.config.alilmTalk.NcpHelper\nimport band.gosrock.parameter.EventJobParameter\nimport org.slf4j.LoggerFactory\nimport org.springframework.batch.core.Job\nimport org.springframework.batch.core.Step\nimport org.springframework.batch.core.configuration.annotation.JobBuilderFactory\nimport org.springframework.batch.core.configuration.annotation.JobScope\nimport org.springframework.batch.core.configuration.annotation.StepBuilderFactory\nimport org.springframework.batch.repeat.RepeatStatus\nimport org.springframework.beans.factory.annotation.Qualifier\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\n\n@Configuration\nclass EventSettlementAlimTalkToHost(\n    private val jobBuilderFactory: JobBuilderFactory,\n    private val stepBuilderFactory: StepBuilderFactory,\n    private val eventAdaptor: EventAdaptor,\n    private val hostAdaptor: HostAdaptor,\n    private val userAdaptor: UserAdaptor,\n    private val ncpHelper: NcpHelper,\n    @Qualifier(BEAN_PREFIX + \"eventJobParameter\")\n    private val eventJobParameter: EventJobParameter,\n) {\n    private val log = LoggerFactory.getLogger(EventSettlementAlimTalkToHost::class.java)\n\n    companion object {\n        private const val JOB_NAME = \"이벤트정산_알림톡발송_호스트\"\n        const val BEAN_PREFIX = \"${JOB_NAME}_\"\n    }\n\n    @Bean(BEAN_PREFIX + \"eventJobParameter\")\n    @JobScope\n    fun eventJobParameter(): EventJobParameter = EventJobParameter(eventAdaptor)\n\n    @Bean(JOB_NAME)\n    fun slackUserStatisticJob(): Job =\n        jobBuilderFactory.get(JOB_NAME).preventRestart().start(eventSettlement()).build()\n\n    @Bean(BEAN_PREFIX + \"step\")\n    @JobScope\n    fun eventSettlement(): Step =\n        stepBuilderFactory\n            .get(BEAN_PREFIX + \"step\")\n            .tasklet { _, _ ->\n                log.info(\">>>>> 정산서 전송 안내 알림톡 스탭\")\n                val event = eventJobParameter.getEvent()\n                val host = hostAdaptor.findById(event.hostId!!)\n                val masterUser = userAdaptor.queryUser(host.masterUserId!!)\n\n                val to = masterUser.profile!!.phoneNumberVo!!.getNaverSmsToNumber()\n                val content = SettlementKakaoTalkAlarm.creationOf(masterUser.profile!!.name!!)\n\n                ncpHelper.sendSettlementNcpAlimTalk(\n                    to,\n                    SettlementKakaoTalkAlarm.creationTemplateCode(),\n                    content,\n                    SettlementKakaoTalkAlarm.creationHeaderOf(),\n                    masterUser.profile!!.email!!,\n                    event.eventBasic!!.name!!,\n                )\n                RepeatStatus.FINISHED\n            }\n            .build()\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/job/EventSettlementEmailToAdmin.kt",
    "content": "package band.gosrock.job\n\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.helper.SettlementEmailHelper\nimport band.gosrock.parameter.EventJobParameter\nimport org.slf4j.LoggerFactory\nimport org.springframework.batch.core.Job\nimport org.springframework.batch.core.Step\nimport org.springframework.batch.core.configuration.annotation.JobBuilderFactory\nimport org.springframework.batch.core.configuration.annotation.JobScope\nimport org.springframework.batch.core.configuration.annotation.StepBuilderFactory\nimport org.springframework.batch.repeat.RepeatStatus\nimport org.springframework.beans.factory.annotation.Qualifier\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\n\n@Configuration\nclass EventSettlementEmailToAdmin(\n    private val jobBuilderFactory: JobBuilderFactory,\n    private val stepBuilderFactory: StepBuilderFactory,\n    private val eventAdaptor: EventAdaptor,\n    private val settlementEmailHelper: SettlementEmailHelper,\n    @Qualifier(BEAN_PREFIX + \"eventJobParameter\")\n    private val eventJobParameter: EventJobParameter,\n) {\n    private val log = LoggerFactory.getLogger(EventSettlementEmailToAdmin::class.java)\n\n    companion object {\n        private const val JOB_NAME = \"이벤트정산_이메일발송_어드민\"\n        const val BEAN_PREFIX = \"${JOB_NAME}_\"\n    }\n\n    @Bean(BEAN_PREFIX + \"eventJobParameter\")\n    @JobScope\n    fun eventJobParameter(): EventJobParameter = EventJobParameter(eventAdaptor)\n\n    @Bean(JOB_NAME)\n    fun slackUserStatisticJob(): Job =\n        jobBuilderFactory.get(JOB_NAME).preventRestart().start(userStatisticStep()).build()\n\n    @Bean(BEAN_PREFIX + \"step\")\n    @JobScope\n    fun userStatisticStep(): Step =\n        stepBuilderFactory\n            .get(BEAN_PREFIX + \"step\")\n            .tasklet { _, _ ->\n                val event = eventJobParameter.getEvent()\n                settlementEmailHelper.sendToAdmin(event)\n                RepeatStatus.FINISHED\n            }\n            .build()\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/job/EventSettlementEmailToHost.kt",
    "content": "package band.gosrock.job\n\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.helper.SettlementEmailHelper\nimport band.gosrock.parameter.EventJobParameter\nimport org.slf4j.LoggerFactory\nimport org.springframework.batch.core.Job\nimport org.springframework.batch.core.Step\nimport org.springframework.batch.core.configuration.annotation.JobBuilderFactory\nimport org.springframework.batch.core.configuration.annotation.JobScope\nimport org.springframework.batch.core.configuration.annotation.StepBuilderFactory\nimport org.springframework.batch.repeat.RepeatStatus\nimport org.springframework.beans.factory.annotation.Qualifier\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\n\n@Configuration\nclass EventSettlementEmailToHost(\n    private val jobBuilderFactory: JobBuilderFactory,\n    private val stepBuilderFactory: StepBuilderFactory,\n    private val eventAdaptor: EventAdaptor,\n    private val hostAdaptor: HostAdaptor,\n    private val userAdaptor: UserAdaptor,\n    private val settlementEmailHelper: SettlementEmailHelper,\n    @Qualifier(BEAN_PREFIX + \"eventJobParameter\")\n    private val eventJobParameter: EventJobParameter,\n) {\n    private val log = LoggerFactory.getLogger(EventSettlementEmailToHost::class.java)\n\n    companion object {\n        private const val JOB_NAME = \"이벤트정산_이메일발송_호스트\"\n        const val BEAN_PREFIX = \"${JOB_NAME}_\"\n    }\n\n    @Bean(BEAN_PREFIX + \"eventJobParameter\")\n    @JobScope\n    fun eventJobParameter(): EventJobParameter = EventJobParameter(eventAdaptor)\n\n    @Bean(JOB_NAME)\n    fun slackUserStatisticJob(): Job =\n        jobBuilderFactory.get(JOB_NAME).preventRestart().start(userStatisticStep()).build()\n\n    @Bean(BEAN_PREFIX + \"step\")\n    @JobScope\n    fun userStatisticStep(): Step =\n        stepBuilderFactory\n            .get(BEAN_PREFIX + \"step\")\n            .tasklet { _, _ ->\n                val event = eventJobParameter.getEvent()\n                val host = hostAdaptor.findById(event.hostId!!)\n                val masterUser = userAdaptor.queryUser(host.masterUserId!!)\n                settlementEmailHelper.sendToHost(event, masterUser.profile!!.email!!)\n                RepeatStatus.FINISHED\n            }\n            .build()\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/job/EventSettlementPDF.kt",
    "content": "package band.gosrock.job\n\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.settlement.adaptor.EventSettlementAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.helper.SettlementPdfHelper\nimport band.gosrock.parameter.EventJobParameter\nimport org.slf4j.LoggerFactory\nimport org.springframework.batch.core.Job\nimport org.springframework.batch.core.Step\nimport org.springframework.batch.core.configuration.annotation.JobBuilderFactory\nimport org.springframework.batch.core.configuration.annotation.JobScope\nimport org.springframework.batch.core.configuration.annotation.StepBuilderFactory\nimport org.springframework.batch.repeat.RepeatStatus\nimport org.springframework.beans.factory.annotation.Qualifier\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\n\n@Configuration\nclass EventSettlementPDF(\n    private val jobBuilderFactory: JobBuilderFactory,\n    private val stepBuilderFactory: StepBuilderFactory,\n    private val eventAdaptor: EventAdaptor,\n    private val hostAdaptor: HostAdaptor,\n    private val userAdaptor: UserAdaptor,\n    private val eventSettlementAdaptor: EventSettlementAdaptor,\n    private val settlementPdfHelper: SettlementPdfHelper,\n    @Qualifier(BEAN_PREFIX + \"eventJobParameter\")\n    private val eventJobParameter: EventJobParameter,\n) {\n    private val log = LoggerFactory.getLogger(EventSettlementPDF::class.java)\n\n    companion object {\n        private const val JOB_NAME = \"이벤트정산서\"\n        const val BEAN_PREFIX = \"${JOB_NAME}_\"\n    }\n\n    @Bean(BEAN_PREFIX + \"eventJobParameter\")\n    @JobScope\n    fun eventJobParameter(): EventJobParameter = EventJobParameter(eventAdaptor)\n\n    @Bean(JOB_NAME)\n    fun slackUserStatisticJob(): Job =\n        jobBuilderFactory.get(JOB_NAME).preventRestart().start(userStatisticStep()).build()\n\n    @Bean(BEAN_PREFIX + \"step\")\n    @JobScope\n    fun userStatisticStep(): Step =\n        stepBuilderFactory\n            .get(BEAN_PREFIX + \"step\")\n            .tasklet { _, _ ->\n                val event = eventJobParameter.getEvent()\n                val eventId = event.id!!\n                val host = hostAdaptor.findById(event.hostId!!)\n                val masterUser = userAdaptor.queryUser(host.masterUserId!!)\n                val eventSettlement = eventSettlementAdaptor.findByEventId(eventId)\n                settlementPdfHelper.uploadPdfToS3(event, eventSettlement, masterUser)\n                RepeatStatus.FINISHED\n            }\n            .build()\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/job/EventSummarySettlement.kt",
    "content": "package band.gosrock.job\n\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.settlement.adaptor.EventSettlementAdaptor\nimport band.gosrock.domain.domains.settlement.service.EventSettlementDomainService\nimport band.gosrock.parameter.EventJobParameter\nimport org.slf4j.LoggerFactory\nimport org.springframework.batch.core.Job\nimport org.springframework.batch.core.Step\nimport org.springframework.batch.core.configuration.annotation.JobBuilderFactory\nimport org.springframework.batch.core.configuration.annotation.JobScope\nimport org.springframework.batch.core.configuration.annotation.StepBuilderFactory\nimport org.springframework.batch.repeat.RepeatStatus\nimport org.springframework.beans.factory.annotation.Qualifier\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\n\n/** 이벤트 정산 수수료 , 내역등을 요약해서 저장합니다. */\n@Configuration\nclass EventSummarySettlement(\n    private val jobBuilderFactory: JobBuilderFactory,\n    private val stepBuilderFactory: StepBuilderFactory,\n    private val eventAdaptor: EventAdaptor,\n    private val orderAdaptor: OrderAdaptor,\n    @Qualifier(BEAN_PREFIX + \"eventJobParameter\")\n    private val eventJobParameter: EventJobParameter,\n    private val eventSettlementAdaptor: EventSettlementAdaptor,\n    private val eventSettlementDomainService: EventSettlementDomainService,\n) {\n    private val log = LoggerFactory.getLogger(EventSummarySettlement::class.java)\n\n    companion object {\n        private const val JOB_NAME = \"이벤트정산요약\"\n        const val BEAN_PREFIX = \"${JOB_NAME}_\"\n    }\n\n    @Bean(BEAN_PREFIX + \"eventJobParameter\")\n    @JobScope\n    fun eventJobParameter(): EventJobParameter = EventJobParameter(eventAdaptor)\n\n    @Bean(JOB_NAME)\n    fun slackUserStatisticJob(): Job =\n        jobBuilderFactory.get(JOB_NAME).preventRestart().start(eventSettlement()).build()\n\n    @Bean(BEAN_PREFIX + \"step\")\n    @JobScope\n    fun eventSettlement(): Step =\n        stepBuilderFactory\n            .get(BEAN_PREFIX + \"step\")\n            .tasklet { _, _ ->\n                val event = eventJobParameter.getEvent()\n                val eventId = event.id!!\n                eventSettlementAdaptor.deleteByEventId(eventId)\n                val orders = orderAdaptor.findByEventId(eventId)\n                eventSettlementDomainService.generateEventSettlement(eventId, orders)\n                RepeatStatus.FINISHED\n            }\n            .build()\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/job/EventTransactionSettlement.kt",
    "content": "package band.gosrock.job\n\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.settlement.adaptor.TransactionSettlementAdaptor\nimport band.gosrock.domain.domains.settlement.domain.TransactionSettlement\nimport band.gosrock.infrastructure.outer.api.tossPayments.client.SettlementClient\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.response.SettlementResponse\nimport band.gosrock.parameter.EventJobParameter\nimport org.slf4j.LoggerFactory\nimport org.springframework.batch.core.Job\nimport org.springframework.batch.core.Step\nimport org.springframework.batch.core.configuration.annotation.JobBuilderFactory\nimport org.springframework.batch.core.configuration.annotation.JobScope\nimport org.springframework.batch.core.configuration.annotation.StepBuilderFactory\nimport org.springframework.batch.repeat.RepeatStatus\nimport org.springframework.beans.factory.annotation.Qualifier\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\n\n/** 토스페이먼츠 거래 내역들을 이벤트별로 취합 후 저장합니다. */\n@Configuration\nclass EventTransactionSettlement(\n    private val jobBuilderFactory: JobBuilderFactory,\n    private val stepBuilderFactory: StepBuilderFactory,\n    private val eventAdaptor: EventAdaptor,\n    private val orderAdaptor: OrderAdaptor,\n    @Qualifier(BEAN_PREFIX + \"eventJobParameter\")\n    private val eventJobParameter: EventJobParameter,\n    private val settlementClient: SettlementClient,\n    private val transactionSettlementAdaptor: TransactionSettlementAdaptor,\n) {\n    private val log = LoggerFactory.getLogger(EventTransactionSettlement::class.java)\n\n    companion object {\n        private const val JOB_NAME = \"이벤트거래정산\"\n        const val BEAN_PREFIX = \"${JOB_NAME}_\"\n    }\n\n    @Bean(BEAN_PREFIX + \"eventJobParameter\")\n    @JobScope\n    fun eventJobParameter(): EventJobParameter = EventJobParameter(eventAdaptor)\n\n    @Bean(JOB_NAME)\n    fun slackUserStatisticJob(): Job =\n        jobBuilderFactory.get(JOB_NAME).preventRestart().start(eventSettlement()).build()\n\n    @Bean(BEAN_PREFIX + \"step\")\n    @JobScope\n    fun eventSettlement(): Step =\n        stepBuilderFactory\n            .get(BEAN_PREFIX + \"step\")\n            .tasklet { _, _ ->\n                val event = eventJobParameter.getEvent()\n                val eventId = event.id!!\n                // 멱등성 유지하기위해 저장된 정산 목록에서 지움 ( 실제로 다시만들일은 없을듯...? )\n                transactionSettlementAdaptor.deleteByEventId(eventId)\n                // 정산 정보 저장\n                transactionSettlementAdaptor.saveAll(getTransactionSettlements(event))\n                RepeatStatus.FINISHED\n            }\n            .build()\n\n    private fun getPaymentOrderUUIDs(eventId: Long): List<String> {\n        val orders = orderAdaptor.findByEventId(eventId)\n        return orders\n            .filter { it.isPaid() }\n            .map { it.pgPaymentInfo.paymentKey }\n    }\n\n    private fun getTransactionSettlements(event: Event): List<TransactionSettlement> {\n        val eventId = event.id!!\n        val paymentOrderUuids = getPaymentOrderUUIDs(eventId)\n        val settlements = getTossPaymentsSettlementData(event)\n\n        return settlements\n            .filter { paymentOrderUuids.contains(it.paymentKey) }\n            .map { TransactionSettlement.of(eventId, it) }\n    }\n\n    // 토스페이먼츠에서 시작일 종료일 매출일 기준 정산액을 조회합니다.\n    private fun getTossPaymentsSettlementData(event: Event): List<SettlementResponse> {\n        val startAt = event.createdAtKt().toLocalDate()\n        val endAt = event.getEndAt()!!.toLocalDate()\n        return settlementClient.execute(startAt, endAt, \"soldDate\", 1, 10000)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/job/SlackUserStatistic.kt",
    "content": "package band.gosrock.job\n\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.helper.slack.SlackUserNotificationSender\nimport band.gosrock.parameter.DateJobParameter\nimport java.time.LocalTime\nimport org.slf4j.LoggerFactory\nimport org.springframework.batch.core.Job\nimport org.springframework.batch.core.Step\nimport org.springframework.batch.core.configuration.annotation.JobBuilderFactory\nimport org.springframework.batch.core.configuration.annotation.JobScope\nimport org.springframework.batch.core.configuration.annotation.StepBuilderFactory\nimport org.springframework.batch.repeat.RepeatStatus\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\n\n@Configuration\nclass SlackUserStatistic(\n    private val jobBuilderFactory: JobBuilderFactory,\n    private val stepBuilderFactory: StepBuilderFactory,\n    private val slackSender: SlackUserNotificationSender,\n    private val userAdaptor: UserAdaptor,\n    private val dateJobParameter: DateJobParameter,\n) {\n    private val log = LoggerFactory.getLogger(SlackUserStatistic::class.java)\n\n    companion object {\n        private const val JOB_NAME = \"슬랙유저통계\"\n        private const val BEAN_PREFIX = \"${JOB_NAME}_\"\n    }\n\n    @Bean(BEAN_PREFIX + \"dateJobParameter\")\n    @JobScope\n    fun dateJobParameter(): DateJobParameter = DateJobParameter()\n\n    @Bean(JOB_NAME)\n    fun slackUserStatisticJob(): Job =\n        jobBuilderFactory\n            .get(JOB_NAME)\n            .preventRestart()\n            // 파라미터로 version 정보를 넘겨주면 개발환경에서 계속 돌려볼수 있다.\n            // 젠킨스의 경우 매번 달라지는 환경변수인 BUILD_ID 를 제공한다.\n            // https://jojoldu.tistory.com/487 맨밑 글 참조\n            .start(userStatisticStep())\n            .build()\n\n    @Bean(BEAN_PREFIX + \"step\")\n    @JobScope\n    fun userStatisticStep(): Step =\n        stepBuilderFactory\n            .get(BEAN_PREFIX + \"step\")\n            .tasklet { _, _ ->\n                log.info(\">>>>> 슬랙 유저 통계 스탭\")\n                val date = dateJobParameter.getDate()\n                val today = date.atTime(LocalTime.MAX)\n                val yesterday = today.minusDays(1L)\n\n                val todayCount = userAdaptor.countNormalUserCreatedBefore(today)\n                val yesterdayCount = userAdaptor.countNormalUserCreatedBefore(yesterday)\n\n                slackSender.execute(date, todayCount, yesterdayCount)\n                RepeatStatus.FINISHED\n            }\n            .build()\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/parameter/DateJobParameter.kt",
    "content": "package band.gosrock.parameter\n\nimport java.time.LocalDate\nimport java.time.format.DateTimeFormatter\nimport org.springframework.batch.core.JobParametersInvalidException\nimport org.springframework.beans.factory.annotation.Value\n\n// https://www.youtube.com/watch?v=_nkJkWVH-mo\n// 우아한 스프링 배치 이동욱님 22분 참조.\nclass DateJobParameter {\n\n    private var _date: LocalDate? = null\n\n    // 넘겨온 파라미터는 String 형이고,\n    // JobScope 가 레이지로 초기화된다는 점을 이용해서\n    // 넘겨온 파라미터를 date 형태로 형 변환 해서 받는다.\n    @Value(\"#{jobParameters[date]}\")\n    fun setDate(date: String?) {\n        if (date == null) {\n            throw JobParametersInvalidException(\"날짜형식의 파라미터가 필요합니다.\")\n        }\n        val formatter = DateTimeFormatter.ofPattern(\"yyyy-MM-dd\")\n        this._date = LocalDate.parse(date, formatter)\n    }\n\n    fun getDate(): LocalDate = _date!!\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/parameter/DateTimeJobParameter.kt",
    "content": "package band.gosrock.parameter\n\nimport java.time.LocalDateTime\nimport java.time.format.DateTimeFormatter\nimport org.springframework.batch.core.JobParametersInvalidException\nimport org.springframework.beans.factory.annotation.Value\n\nclass DateTimeJobParameter {\n\n    private var _time: LocalDateTime? = null\n\n    @Value(\"#{jobParameters[dateTime]}\")\n    fun setDateTime(dateTime: String?) {\n        // 인자가 없다면 now 로 설정\n        if (dateTime == null) {\n            this._time = LocalDateTime.now()\n        } else {\n            try {\n                val formatter = DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm\")\n                this._time = LocalDateTime.parse(dateTime, formatter)\n            } catch (e: Exception) {\n                throw JobParametersInvalidException(\"올바르지 않은 시간 형식입니다\")\n            }\n        }\n    }\n\n    fun getTime(): LocalDateTime = _time!!\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/kotlin/band/gosrock/parameter/EventJobParameter.kt",
    "content": "package band.gosrock.parameter\n\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.domain.Event\nimport org.springframework.batch.core.JobParametersInvalidException\nimport org.springframework.beans.factory.annotation.Value\n\nclass EventJobParameter(\n    private val eventAdaptor: EventAdaptor,\n) {\n\n    private var _event: Event? = null\n\n    @Value(\"#{jobParameters[eventId]}\")\n    fun setDate(eventId: Long?) {\n        if (eventId == null) {\n            throw JobParametersInvalidException(\"이벤트 아이디가 필요합니다.\")\n        }\n        this._event = eventAdaptor.findById(eventId)\n    }\n\n    fun getEvent(): Event = _event!!\n}\n"
  },
  {
    "path": "DuDoong-Batch/src/main/resources/application.yml",
    "content": "# commons\nspring:\n  profiles:\n    include:\n      - infrastructure\n      - domain\n      - common\n  batch.job.names: ${job.name:NONE}\n\n---\nspring:\n  config:\n    activate:\n      on-profile: dev\n#logging:\n#  level:\n#    root: info\n#logging:\n#  level:\n#    org.springframework.data.*.*: debug\n#    org.springframework.cache.*: debug\n---\nspring:\n  config:\n    activate:\n      on-profile: staging\n\n---\nspring:\n  config:\n    activate:\n      on-profile: prod\n"
  },
  {
    "path": "DuDoong-Common/build.gradle.kts",
    "content": "dependencies {\n    implementation(\"io.jsonwebtoken:jjwt-api:0.12.3\")\n    implementation(\"org.springframework.boot:spring-boot-starter-validation\")\n    implementation(\"com.fasterxml.jackson.core:jackson-annotations\")\n    implementation(\"com.fasterxml.jackson.core:jackson-databind\")\n    runtimeOnly(\"io.jsonwebtoken:jjwt-impl:0.12.3\")\n    runtimeOnly(\"io.jsonwebtoken:jjwt-jackson:0.12.3\")\n    api(\"org.springframework.boot:spring-boot-starter-aop\")\n}\n\ntasks.bootJar { enabled = false }\ntasks.jar { enabled = true }\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/DuDoongCommonApplication.kt",
    "content": "package band.gosrock.common\n\nimport org.springframework.boot.autoconfigure.SpringBootApplication\n\n// 테스팅 용도 어플리케이션입니다.\n@SpringBootApplication\nclass DuDoongCommonApplication\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/Adaptor.kt",
    "content": "package band.gosrock.common.annotation\n\nimport org.springframework.core.annotation.AliasFor\nimport org.springframework.stereotype.Component\n\n@Target(AnnotationTarget.CLASS)\n@Retention(AnnotationRetention.RUNTIME)\n@MustBeDocumented\n@Component\nannotation class Adaptor(\n    @get:AliasFor(annotation = Component::class)\n    val value: String = \"\",\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/ApiErrorCodeExample.kt",
    "content": "package band.gosrock.common.annotation\n\nimport band.gosrock.common.exception.BaseErrorCode\nimport kotlin.reflect.KClass\n\n@Target(AnnotationTarget.FUNCTION)\n@Retention(AnnotationRetention.RUNTIME)\nannotation class ApiErrorCodeExample(\n    val value: KClass<out BaseErrorCode>,\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/ApiErrorExceptionsExample.kt",
    "content": "package band.gosrock.common.annotation\n\nimport band.gosrock.common.interfaces.SwaggerExampleExceptions\nimport kotlin.reflect.KClass\n\n@Target(AnnotationTarget.FUNCTION)\n@Retention(AnnotationRetention.RUNTIME)\nannotation class ApiErrorExceptionsExample(\n    val value: KClass<out SwaggerExampleExceptions>,\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/CurrentUserId.kt",
    "content": "package band.gosrock.common.annotation\n\n@Target(AnnotationTarget.VALUE_PARAMETER)\n@Retention(AnnotationRetention.RUNTIME)\nannotation class CurrentUserId\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/DateFormat.kt",
    "content": "package band.gosrock.common.annotation\n\nimport com.fasterxml.jackson.annotation.JacksonAnnotationsInside\nimport com.fasterxml.jackson.annotation.JsonFormat\n\n@JacksonAnnotationsInside\n@Retention(AnnotationRetention.RUNTIME)\n@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy.MM.dd HH:mm\", timezone = \"Asia/Seoul\")\nannotation class DateFormat\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/DevelopOnlyApi.kt",
    "content": "package band.gosrock.common.annotation\n\n@Target(AnnotationTarget.FUNCTION)\n@Retention(AnnotationRetention.RUNTIME)\nannotation class DevelopOnlyApi\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/DisableSwaggerSecurity.kt",
    "content": "package band.gosrock.common.annotation\n\n@Target(AnnotationTarget.FUNCTION)\n@Retention(AnnotationRetention.RUNTIME)\nannotation class DisableSwaggerSecurity\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/DomainService.kt",
    "content": "package band.gosrock.common.annotation\n\nimport org.springframework.core.annotation.AliasFor\nimport org.springframework.stereotype.Component\n\n@Target(AnnotationTarget.CLASS)\n@Retention(AnnotationRetention.RUNTIME)\n@MustBeDocumented\n@Component\nannotation class DomainService(\n    @get:AliasFor(annotation = Component::class)\n    val value: String = \"\",\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/Enum.kt",
    "content": "package band.gosrock.common.annotation\n\nimport band.gosrock.common.validator.EnumValidator\nimport jakarta.validation.Constraint\nimport jakarta.validation.Payload\nimport kotlin.reflect.KClass\n\n/** RequestBody 의 Enum 검증을 위한 어노테이션 입니다 */\n@Constraint(validatedBy = [EnumValidator::class])\n@Target(AnnotationTarget.FUNCTION, AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)\n@Retention(AnnotationRetention.RUNTIME)\nannotation class Enum(\n    val message: String = \"Invalid Enum Value.\",\n    val groups: Array<KClass<*>> = [],\n    val payload: Array<KClass<out Payload>> = [],\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/EnumClass.kt",
    "content": "package band.gosrock.common.annotation\n\nimport band.gosrock.common.deserializer.CustomEnumDeserializer\nimport com.fasterxml.jackson.annotation.JacksonAnnotationsInside\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize\n\n@Target(AnnotationTarget.CLASS)\n@Retention(AnnotationRetention.RUNTIME)\n@JacksonAnnotationsInside\n@JsonDeserialize(using = CustomEnumDeserializer::class)\nannotation class EnumClass\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/ExceptionDoc.kt",
    "content": "package band.gosrock.common.annotation\n\nimport org.springframework.core.annotation.AliasFor\nimport org.springframework.stereotype.Component\n\n@Target(AnnotationTarget.CLASS)\n@Retention(AnnotationRetention.RUNTIME)\n@MustBeDocumented\n@Component\nannotation class ExceptionDoc(\n    @get:AliasFor(annotation = Component::class)\n    val value: String = \"\",\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/ExplainError.kt",
    "content": "package band.gosrock.common.annotation\n\nimport org.springframework.stereotype.Component\n\n@Target(AnnotationTarget.FIELD)\n@Retention(AnnotationRetention.RUNTIME)\n@MustBeDocumented\n@Component\nannotation class ExplainError(\n    val value: String = \"\",\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/Helper.kt",
    "content": "package band.gosrock.common.annotation\n\nimport org.springframework.core.annotation.AliasFor\nimport org.springframework.stereotype.Component\n\n@Target(AnnotationTarget.CLASS)\n@Retention(AnnotationRetention.RUNTIME)\n@MustBeDocumented\n@Component\nannotation class Helper(\n    @get:AliasFor(annotation = Component::class)\n    val value: String = \"\",\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/Mapper.kt",
    "content": "package band.gosrock.common.annotation\n\nimport org.springframework.core.annotation.AliasFor\nimport org.springframework.stereotype.Component\n\n@Target(AnnotationTarget.CLASS)\n@Retention(AnnotationRetention.RUNTIME)\n@MustBeDocumented\n@Component\nannotation class Mapper(\n    @get:AliasFor(annotation = Component::class)\n    val value: String = \"\",\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/Phone.kt",
    "content": "package band.gosrock.common.annotation\n\nimport band.gosrock.common.validator.PhoneValidator\nimport jakarta.validation.Constraint\nimport jakarta.validation.Payload\nimport kotlin.reflect.KClass\n\n@Target(AnnotationTarget.FIELD)\n@Retention(AnnotationRetention.RUNTIME)\n@Constraint(validatedBy = [PhoneValidator::class])\nannotation class Phone(\n    val message: String = \"\",\n    val groups: Array<KClass<*>> = [],\n    val payload: Array<KClass<out Payload>> = [],\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/Policy.kt",
    "content": "package band.gosrock.common.annotation\n\nimport org.springframework.core.annotation.AliasFor\nimport org.springframework.stereotype.Component\n\n@Target(AnnotationTarget.CLASS)\n@Retention(AnnotationRetention.RUNTIME)\n@MustBeDocumented\n@Component\nannotation class Policy(\n    @get:AliasFor(annotation = Component::class)\n    val value: String = \"\",\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/Port.kt",
    "content": "package band.gosrock.common.annotation\n\nimport org.springframework.core.annotation.AliasFor\nimport org.springframework.stereotype.Component\n\n@Target(AnnotationTarget.CLASS)\n@Retention(AnnotationRetention.RUNTIME)\n@MustBeDocumented\n@Component\nannotation class Port(\n    @get:AliasFor(annotation = Component::class)\n    val value: String = \"\",\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/UseCase.kt",
    "content": "package band.gosrock.common.annotation\n\nimport org.springframework.core.annotation.AliasFor\nimport org.springframework.stereotype.Component\n\n@Target(AnnotationTarget.CLASS)\n@Retention(AnnotationRetention.RUNTIME)\n@MustBeDocumented\n@Component\nannotation class UseCase(\n    @get:AliasFor(annotation = Component::class)\n    val value: String = \"\",\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/annotation/Validator.kt",
    "content": "package band.gosrock.common.annotation\n\nimport org.springframework.core.annotation.AliasFor\nimport org.springframework.stereotype.Component\n\n@Target(AnnotationTarget.CLASS)\n@Retention(AnnotationRetention.RUNTIME)\n@MustBeDocumented\n@Component\nannotation class Validator(\n    @get:AliasFor(annotation = Component::class)\n    val value: String = \"\",\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/aop/ApiBlockingAspect.kt",
    "content": "package band.gosrock.common.aop\n\nimport band.gosrock.common.exception.DuDoongDynamicException\nimport band.gosrock.common.helper.SpringEnvironmentHelper\nimport org.aspectj.lang.ProceedingJoinPoint\nimport org.aspectj.lang.annotation.Around\nimport org.aspectj.lang.annotation.Aspect\nimport org.slf4j.LoggerFactory\nimport org.springframework.stereotype.Component\n\n@Aspect\n@Component\nclass ApiBlockingAspect(\n    private val springEnvironmentHelper: SpringEnvironmentHelper,\n) {\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    @Around(\"@annotation(band.gosrock.common.annotation.DevelopOnlyApi)\")\n    @Throws(Throwable::class)\n    fun checkApiAcceptingCondition(joinPoint: ProceedingJoinPoint): Any? {\n        if (springEnvironmentHelper.isProdProfile()) {\n            throw DuDoongDynamicException(405, \"Blocked Api\", \"not working api in production\")\n        }\n        return joinPoint.proceed()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/config/ConfigurationPropertiesConfig.kt",
    "content": "package band.gosrock.common.config\n\nimport band.gosrock.common.properties.JwtProperties\nimport band.gosrock.common.properties.OauthProperties\nimport band.gosrock.common.properties.TossPaymentsProperties\nimport org.springframework.boot.context.properties.EnableConfigurationProperties\nimport org.springframework.context.annotation.Configuration\n\n@EnableConfigurationProperties(\n    JwtProperties::class,\n    OauthProperties::class,\n    TossPaymentsProperties::class,\n)\n@Configuration\nclass ConfigurationPropertiesConfig\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/consts/DuDoongStatic.kt",
    "content": "package band.gosrock.common.consts\n\nobject DuDoongStatic {\n    const val AUTH_HEADER = \"Authorization\"\n    const val BEARER = \"Bearer \"\n    const val WITHDRAW_PREFIX = \"DELETED:\"\n    const val TOKEN_TYPE = \"type\"\n    const val ADMIN_AUDIENCE = \"admin\"\n    const val ADMIN_TOKEN_HEADER = \"X-Admin-Token\"\n    const val TOKEN_ISSUER = \"DuDoong\"\n    const val ACCESS_TOKEN = \"ACCESS_TOKEN\"\n    const val REFRESH_TOKEN = \"REFRESH_TOKEN\"\n    const val KR_YES = \"예\"\n    const val KR_NO = \"아니요\"\n\n    const val MILLI_TO_SECOND = 1000\n    const val BAD_REQUEST = 400\n    const val UNAUTHORIZED = 401\n    const val FORBIDDEN = 403\n    const val NOT_FOUND = 404\n    const val INTERNAL_SERVER = 500\n\n    const val NO_START_NUMBER = 1000000L\n    const val MINIMUM_PAYMENT_WON = 1000L\n    const val ZERO = 0L\n\n    const val assetDomain = \"https://asset.dudoong.com/\"\n\n    const val KAKAO_OAUTH_QUERY_STRING =\n        \"/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code\"\n\n    @JvmField\n    val SwaggerPatterns = arrayOf(\n        \"/swagger-resources/**\",\n        \"/swagger-ui/**\",\n        \"/v3/api-docs/**\",\n        \"/v3/api-docs\",\n    )\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/deserializer/CustomEnumDeserializer.kt",
    "content": "package band.gosrock.common.deserializer\n\nimport com.fasterxml.jackson.core.JsonParser\nimport com.fasterxml.jackson.databind.BeanProperty\nimport com.fasterxml.jackson.databind.DeserializationContext\nimport com.fasterxml.jackson.databind.JsonDeserializer\nimport com.fasterxml.jackson.databind.JsonMappingException\nimport com.fasterxml.jackson.databind.deser.ContextualDeserializer\nimport com.fasterxml.jackson.databind.deser.std.StdDeserializer\n\nclass CustomEnumDeserializer(vc: Class<*>? = null) :\n    StdDeserializer<Enum<*>>(vc),\n    ContextualDeserializer {\n\n    @Suppress(\"UNCHECKED_CAST\")\n    override fun deserialize(jp: JsonParser, ctxt: DeserializationContext): Enum<*>? {\n        val jsonNode = jp.codec.readTree<com.fasterxml.jackson.databind.JsonNode>(jp)\n        val text = jsonNode.asText()\n        val enumType = _valueClass as? Class<out Enum<*>> ?: return null\n        return enumType.enumConstants?.firstOrNull { it.name == text }\n    }\n\n    @Throws(JsonMappingException::class)\n    override fun createContextual(\n        ctxt: DeserializationContext,\n        property: BeanProperty,\n    ): JsonDeserializer<*> {\n        return CustomEnumDeserializer(property.type.rawClass)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/dto/AccessTokenInfo.kt",
    "content": "package band.gosrock.common.dto\n\ndata class AccessTokenInfo(\n    val userId: Long,\n    val isAdmin: Boolean = false,\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/dto/ErrorReason.kt",
    "content": "package band.gosrock.common.dto\n\ndata class ErrorReason(\n    val status: Int,\n    val code: String,\n    val reason: String,\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/dto/ErrorResponse.kt",
    "content": "package band.gosrock.common.dto\n\nimport java.time.LocalDateTime\n\nclass ErrorResponse {\n    val success: Boolean = false\n    val status: Int\n    val code: String\n    val reason: String\n    val timeStamp: LocalDateTime\n    val path: String\n\n    constructor(errorReason: ErrorReason, path: String) {\n        this.status = errorReason.status\n        this.code = errorReason.code\n        this.reason = errorReason.reason\n        this.timeStamp = LocalDateTime.now()\n        this.path = path\n    }\n\n    constructor(status: Int, code: String, reason: String, path: String) {\n        this.status = status\n        this.code = code\n        this.reason = reason\n        this.timeStamp = LocalDateTime.now()\n        this.path = path\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/dto/OIDCDecodePayload.kt",
    "content": "package band.gosrock.common.dto\n\ndata class OIDCDecodePayload(\n    /** issuer ex https://kauth.kakao.com */\n    val iss: String,\n    /** client id */\n    val aud: String,\n    /** oauth provider account unique id */\n    val sub: String,\n    val email: String,\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/dto/SuccessResponse.kt",
    "content": "package band.gosrock.common.dto\n\nimport java.time.LocalDateTime\n\nclass SuccessResponse(\n    val status: Int,\n    val data: Any?,\n) {\n    val success: Boolean = true\n    val timeStamp: LocalDateTime = LocalDateTime.now()\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/exception/BadFileExtensionException.kt",
    "content": "package band.gosrock.common.exception\n\nclass BadFileExtensionException private constructor() : DuDoongCodeException(GlobalErrorCode.BAD_FILE_EXTENSION) {\n    companion object {\n@JvmField\n        val EXCEPTION: DuDoongCodeException = BadFileExtensionException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/exception/BadLockIdentifierException.kt",
    "content": "package band.gosrock.common.exception\n\nclass BadLockIdentifierException private constructor() : DuDoongCodeException(GlobalErrorCode.BAD_LOCK_IDENTIFIER) {\n    companion object {\n@JvmField\n        val EXCEPTION: DuDoongCodeException = BadLockIdentifierException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/exception/BaseErrorCode.kt",
    "content": "package band.gosrock.common.exception\n\nimport band.gosrock.common.dto.ErrorReason\n\ninterface BaseErrorCode {\n    fun getErrorReason(): ErrorReason\n\n    @Throws(NoSuchFieldException::class)\n    fun getExplainError(): String\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/exception/DuDoongCodeException.kt",
    "content": "package band.gosrock.common.exception\n\nimport band.gosrock.common.dto.ErrorReason\n\nopen class DuDoongCodeException(\n    val errorCode: BaseErrorCode,\n) : RuntimeException() {\n    fun getErrorReason(): ErrorReason = errorCode.getErrorReason()\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/exception/DuDoongDynamicException.kt",
    "content": "package band.gosrock.common.exception\n\nclass DuDoongDynamicException(\n    val status: Int,\n    val code: String,\n    val reason: String,\n) : RuntimeException()\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/exception/ExpiredTokenException.kt",
    "content": "package band.gosrock.common.exception\n\nclass ExpiredTokenException private constructor() : DuDoongCodeException(GlobalErrorCode.TOKEN_EXPIRED) {\n    companion object {\n@JvmField\n        val EXCEPTION: DuDoongCodeException = ExpiredTokenException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/exception/GlobalErrorCode.kt",
    "content": "package band.gosrock.common.exception\n\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.consts.DuDoongStatic.BAD_REQUEST\nimport band.gosrock.common.consts.DuDoongStatic.FORBIDDEN\nimport band.gosrock.common.consts.DuDoongStatic.INTERNAL_SERVER\nimport band.gosrock.common.consts.DuDoongStatic.NOT_FOUND\nimport band.gosrock.common.consts.DuDoongStatic.UNAUTHORIZED\nimport band.gosrock.common.dto.ErrorReason\n\n/**\n * 글로벌 관련 예외 코드들이 나온 곳입니다. 인증 , global, aop 종류등 도메인 제외한 exception 코드들이 모이는 곳입니다.\n * 도메인 관련 Exception code 들은 도메인 내부 exception 패키지에 위치시키면 됩니다.\n */\nenum class GlobalErrorCode(\n    val status: Int,\n    val code: String,\n    val reason: String,\n) : BaseErrorCode {\n    @ExplainError(\"백엔드에서 예시로만든 에러입니다. 개발용!이에유! 신경쓰지마세유\")\n    EXAMPLE_NOT_FOUND(NOT_FOUND, \"EXAMPLE_404_1\", \"예시를 찾을 수 없는 오류입니다.\"),\n\n    @ExplainError(\"밸리데이션 (검증 과정 수행속 ) 발생하는 오류입니다.\")\n    ARGUMENT_NOT_VALID_ERROR(BAD_REQUEST, \"GLOBAL_400_1\", \"검증 오류\"),\n\n    @ExplainError(\"accessToken 만료시 발생하는 오류입니다.\")\n    TOKEN_EXPIRED(UNAUTHORIZED, \"AUTH_401_1\", \"인증 시간이 만료되었습니다. 인증토큰을 재 발급 해주세요\"),\n\n    @ExplainError(\"refreshToken 만료시 발생하는 오류입니다.\")\n    REFRESH_TOKEN_EXPIRED(FORBIDDEN, \"AUTH_403_1\", \"인증 시간이 만료되었습니다. 재 로그인 해주세요.\"),\n\n    @ExplainError(\"헤더에 올바른 accessToken을 담지않았을 때 발생하는 오류(형식 불일치 등)\")\n    ACCESS_TOKEN_NOT_EXIST(FORBIDDEN, \"AUTH_403_2\", \"알맞은 accessToken을 넣어주세요.\"),\n\n    @ExplainError(\"인증 토큰이 잘못됐을 때 발생하는 오류입니다.\")\n    INVALID_TOKEN(UNAUTHORIZED, \"GLOBAL_401_1\", \"잘못된 토큰입니다. 재 로그인 해주세요\"),\n\n    @ExplainError(\"500번대 알수없는 오류입니다. 서버 관리자에게 문의 주세요\")\n    INTERNAL_SERVER_ERROR(INTERNAL_SERVER, \"GLOBAL_500_1\", \"서버 오류. 관리자에게 문의 부탁드립니다.\"),\n\n    OTHER_SERVER_BAD_REQUEST(BAD_REQUEST, \"FEIGN_400_1\", \"Other server bad request\"),\n    OTHER_SERVER_UNAUTHORIZED(BAD_REQUEST, \"FEIGN_400_2\", \"Other server unauthorized\"),\n    OTHER_SERVER_FORBIDDEN(BAD_REQUEST, \"FEIGN_400_3\", \"Other server forbidden\"),\n    OTHER_SERVER_EXPIRED_TOKEN(BAD_REQUEST, \"FEIGN_400_4\", \"Other server expired token\"),\n    OTHER_SERVER_NOT_FOUND(BAD_REQUEST, \"FEIGN_400_5\", \"Other server not found error\"),\n    OTHER_SERVER_INTERNAL_SERVER_ERROR(BAD_REQUEST, \"FEIGN_400_6\", \"Other server internal server error\"),\n    NOT_AVAILABLE_REDISSON_LOCK(500, \"Redisson_500_1\", \"can not get redisson lock\"),\n    SECURITY_CONTEXT_NOT_FOUND(500, \"GLOBAL_500_2\", \"security context not found\"),\n\n    TOSS_PAYMENTS_UNHANDLED(INTERNAL_SERVER, \"PAYMENTS_INTERNAL_SERVER\", \"관리자에게 연락부탁드려요.\"),\n    BAD_LOCK_IDENTIFIER(500, \"AOP_500_1\", \"락의 키값이 잘못 세팅 되었습니다\"),\n    BAD_FILE_EXTENSION(BAD_REQUEST, \"FILE_400_1\", \"파일 확장자가 잘못 되었습니다.\"),\n    TOSS_PAYMENTS_ENUM_NOT_MATCH(INTERNAL_SERVER, \"INFRA_500_1\", \"토스페이먼츠 이넘값 관련 매칭 안된 문제입니다.\"),\n    TOO_MANY_REQUEST(429, \"GLOBAL_429_1\", \"과도한 요청을 보내셨습니다. 잠시 기다려 주세요.\");\n\n    override fun getErrorReason(): ErrorReason =\n        ErrorReason(status = status, code = code, reason = reason)\n\n    override fun getExplainError(): String {\n        val field = this.javaClass.getField(this.name)\n        val annotation = field.getAnnotation(ExplainError::class.java)\n        return annotation?.value ?: this.reason\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/exception/InvalidTokenException.kt",
    "content": "package band.gosrock.common.exception\n\nclass InvalidTokenException private constructor() : DuDoongCodeException(GlobalErrorCode.INVALID_TOKEN) {\n    companion object {\n@JvmField\n        val EXCEPTION: DuDoongCodeException = InvalidTokenException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/exception/NotAvailableRedissonLockException.kt",
    "content": "package band.gosrock.common.exception\n\nclass NotAvailableRedissonLockException private constructor() : DuDoongCodeException(GlobalErrorCode.NOT_AVAILABLE_REDISSON_LOCK) {\n    companion object {\n@JvmField\n        val EXCEPTION: DuDoongCodeException = NotAvailableRedissonLockException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/exception/OtherServerBadRequestException.kt",
    "content": "package band.gosrock.common.exception\n\nclass OtherServerBadRequestException private constructor() : DuDoongCodeException(GlobalErrorCode.OTHER_SERVER_BAD_REQUEST) {\n    companion object {\n@JvmField\n        val EXCEPTION: DuDoongCodeException = OtherServerBadRequestException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/exception/OtherServerExpiredTokenException.kt",
    "content": "package band.gosrock.common.exception\n\nclass OtherServerExpiredTokenException private constructor() : DuDoongCodeException(GlobalErrorCode.OTHER_SERVER_EXPIRED_TOKEN) {\n    companion object {\n@JvmField\n        val EXCEPTION: DuDoongCodeException = OtherServerExpiredTokenException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/exception/OtherServerForbiddenException.kt",
    "content": "package band.gosrock.common.exception\n\nclass OtherServerForbiddenException private constructor() : DuDoongCodeException(GlobalErrorCode.OTHER_SERVER_FORBIDDEN) {\n    companion object {\n@JvmField\n        val EXCEPTION: DuDoongCodeException = OtherServerForbiddenException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/exception/OtherServerInternalSeverErrorException.kt",
    "content": "package band.gosrock.common.exception\n\nclass OtherServerInternalSeverErrorException private constructor() : DuDoongCodeException(GlobalErrorCode.OTHER_SERVER_INTERNAL_SERVER_ERROR) {\n    companion object {\n@JvmField\n        val EXCEPTION: DuDoongCodeException = OtherServerInternalSeverErrorException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/exception/OtherServerNotFoundException.kt",
    "content": "package band.gosrock.common.exception\n\nclass OtherServerNotFoundException private constructor() : DuDoongCodeException(GlobalErrorCode.OTHER_SERVER_NOT_FOUND) {\n    companion object {\n@JvmField\n        val EXCEPTION: DuDoongCodeException = OtherServerNotFoundException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/exception/OtherServerUnauthorizedException.kt",
    "content": "package band.gosrock.common.exception\n\nclass OtherServerUnauthorizedException private constructor() : DuDoongCodeException(GlobalErrorCode.OTHER_SERVER_UNAUTHORIZED) {\n    companion object {\n@JvmField\n        val EXCEPTION: DuDoongCodeException = OtherServerUnauthorizedException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/exception/RefreshTokenExpiredException.kt",
    "content": "package band.gosrock.common.exception\n\nclass RefreshTokenExpiredException private constructor() : DuDoongCodeException(GlobalErrorCode.REFRESH_TOKEN_EXPIRED) {\n    companion object {\n@JvmField\n        val EXCEPTION: DuDoongCodeException = RefreshTokenExpiredException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/exception/SecurityContextNotFoundException.kt",
    "content": "package band.gosrock.common.exception\n\nclass SecurityContextNotFoundException private constructor() : DuDoongCodeException(GlobalErrorCode.SECURITY_CONTEXT_NOT_FOUND) {\n    companion object {\n@JvmField\n        val EXCEPTION: DuDoongCodeException = SecurityContextNotFoundException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/exception/TooManyRequestException.kt",
    "content": "package band.gosrock.common.exception\n\nclass TooManyRequestException private constructor() : DuDoongCodeException(GlobalErrorCode.TOO_MANY_REQUEST) {\n    companion object {\n@JvmField\n        val EXCEPTION: DuDoongCodeException = TooManyRequestException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/helper/SpringEnvironmentHelper.kt",
    "content": "package band.gosrock.common.helper\n\nimport org.springframework.core.env.Environment\nimport org.springframework.stereotype.Component\nimport org.springframework.util.CollectionUtils\n\n@Component\nclass SpringEnvironmentHelper(\n    private val environment: Environment,\n) {\n    private val PROD = \"prod\"\n    private val STAGING = \"staging\"\n    private val DEV = \"dev\"\n    private val PROD_AND_STAGING = listOf(\"staging\", \"prod\")\n\n    fun isProdProfile(): Boolean {\n        val currentProfile = environment.activeProfiles.toList()\n        return currentProfile.contains(PROD)\n    }\n\n    fun isStagingProfile(): Boolean {\n        val currentProfile = environment.activeProfiles.toList()\n        return currentProfile.contains(STAGING)\n    }\n\n    fun isDevProfile(): Boolean {\n        val currentProfile = environment.activeProfiles.toList()\n        return currentProfile.contains(DEV)\n    }\n\n    fun isProdAndStagingProfile(): Boolean {\n        val currentProfile = environment.activeProfiles.toList()\n        return CollectionUtils.containsAny(PROD_AND_STAGING, currentProfile)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/interfaces/SwaggerExampleExceptions.kt",
    "content": "package band.gosrock.common.interfaces\n\nimport band.gosrock.common.annotation.ExceptionDoc\n\n@ExceptionDoc\ninterface SwaggerExampleExceptions\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/jwt/JwtOIDCProvider.kt",
    "content": "package band.gosrock.common.jwt\n\nimport band.gosrock.common.dto.OIDCDecodePayload\nimport band.gosrock.common.exception.ExpiredTokenException\nimport band.gosrock.common.exception.InvalidTokenException\nimport io.jsonwebtoken.Claims\nimport io.jsonwebtoken.ExpiredJwtException\nimport io.jsonwebtoken.Jws\nimport io.jsonwebtoken.Jwts\nimport java.math.BigInteger\nimport java.security.Key\nimport java.security.KeyFactory\nimport java.security.spec.RSAPublicKeySpec\nimport java.util.Base64\nimport org.slf4j.LoggerFactory\nimport org.springframework.stereotype.Component\n\n@Component\nclass JwtOIDCProvider {\n    private val log = LoggerFactory.getLogger(JwtOIDCProvider::class.java)\n    private val KID = \"kid\"\n\n    fun getKidFromUnsignedTokenHeader(token: String, iss: String, aud: String): String {\n        val unsignedToken = getUnsignedToken(token)\n        val splitToken = unsignedToken.split(\".\")\n        val headerJson = String(Base64.getUrlDecoder().decode(splitToken[0]))\n        // Parse kid from header manually since JJWT 0.12 removed unsigned JWT parsing\n        val kidRegex = \"\"\"\"kid\"\\s*:\\s*\"([^\"]+)\"\"\"\".toRegex()\n        val match = kidRegex.find(headerJson) ?: throw InvalidTokenException.EXCEPTION\n        return match.groupValues[1]\n    }\n\n    private fun getUnsignedToken(token: String): String {\n        val splitToken = token.split(\".\")\n        if (splitToken.size != 3) throw InvalidTokenException.EXCEPTION\n        return \"${splitToken[0]}.${splitToken[1]}.\"\n    }\n\n    fun getOIDCTokenJws(token: String, modulus: String, exponent: String): Jws<Claims> =\n        try {\n            Jwts.parser()\n                .verifyWith(getRSAPublicKey(modulus, exponent) as java.security.PublicKey)\n                .build()\n                .parseSignedClaims(token)\n        } catch (e: ExpiredJwtException) {\n            throw ExpiredTokenException.EXCEPTION\n        } catch (e: Exception) {\n            log.error(e.toString())\n            throw InvalidTokenException.EXCEPTION\n        }\n\n    fun getOIDCTokenBody(token: String, modulus: String, exponent: String): OIDCDecodePayload {\n        val body = getOIDCTokenJws(token, modulus, exponent).payload\n        return OIDCDecodePayload(\n            iss = body.issuer,\n            aud = body.audience.firstOrNull() ?: \"\",\n            sub = body.subject,\n            email = body.get(\"email\", String::class.java),\n        )\n    }\n\n    private fun getRSAPublicKey(modulus: String, exponent: String): Key {\n        val keyFactory = KeyFactory.getInstance(\"RSA\")\n        val decodeN = Base64.getUrlDecoder().decode(modulus)\n        val decodeE = Base64.getUrlDecoder().decode(exponent)\n        val n = BigInteger(1, decodeN)\n        val e = BigInteger(1, decodeE)\n        val keySpec = RSAPublicKeySpec(n, e)\n        return keyFactory.generatePublic(keySpec)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/jwt/JwtTokenProvider.kt",
    "content": "package band.gosrock.common.jwt\n\nimport band.gosrock.common.consts.DuDoongStatic.ACCESS_TOKEN\nimport band.gosrock.common.consts.DuDoongStatic.ADMIN_AUDIENCE\nimport band.gosrock.common.consts.DuDoongStatic.MILLI_TO_SECOND\nimport band.gosrock.common.consts.DuDoongStatic.REFRESH_TOKEN\nimport band.gosrock.common.consts.DuDoongStatic.TOKEN_ISSUER\nimport band.gosrock.common.consts.DuDoongStatic.TOKEN_TYPE\nimport band.gosrock.common.dto.AccessTokenInfo\nimport band.gosrock.common.exception.ExpiredTokenException\nimport band.gosrock.common.exception.InvalidTokenException\nimport band.gosrock.common.exception.RefreshTokenExpiredException\nimport band.gosrock.common.properties.JwtProperties\nimport io.jsonwebtoken.Claims\nimport io.jsonwebtoken.ExpiredJwtException\nimport io.jsonwebtoken.Jws\nimport io.jsonwebtoken.Jwts\nimport java.nio.charset.StandardCharsets\nimport java.util.Date\nimport javax.crypto.SecretKey\nimport io.jsonwebtoken.security.Keys\nimport org.springframework.stereotype.Component\n\n@Component\nclass JwtTokenProvider(\n    private val jwtProperties: JwtProperties,\n) {\n    private fun getJws(token: String): Jws<Claims> =\n        try {\n            Jwts.parser()\n                .verifyWith(getSecretKey())\n                .build()\n                .parseSignedClaims(token)\n        } catch (e: ExpiredJwtException) {\n            throw ExpiredTokenException.EXCEPTION\n        } catch (e: Exception) {\n            throw InvalidTokenException.EXCEPTION\n        }\n\n    private fun getSecretKey(): SecretKey =\n        Keys.hmacShaKeyFor(jwtProperties.secretKey.toByteArray(StandardCharsets.UTF_8))\n\n    private fun buildAccessToken(id: Long, issuedAt: Date, expiresIn: Date, audience: String? = null): String =\n        Jwts.builder()\n            .issuer(TOKEN_ISSUER)\n            .issuedAt(issuedAt)\n            .subject(id.toString())\n            .claim(TOKEN_TYPE, ACCESS_TOKEN)\n            .apply { if (audience != null) audience().add(audience).and() }\n            .expiration(expiresIn)\n            .signWith(getSecretKey())\n            .compact()\n\n    private fun buildRefreshToken(id: Long, issuedAt: Date, expiresIn: Date): String =\n        Jwts.builder()\n            .issuer(TOKEN_ISSUER)\n            .issuedAt(issuedAt)\n            .subject(id.toString())\n            .claim(TOKEN_TYPE, REFRESH_TOKEN)\n            .expiration(expiresIn)\n            .signWith(getSecretKey())\n            .compact()\n\n    fun generateAccessToken(id: Long): String {\n        val issuedAt = Date()\n        val accessTokenExpiresIn = Date(issuedAt.time + jwtProperties.accessExp * MILLI_TO_SECOND)\n        return buildAccessToken(id, issuedAt, accessTokenExpiresIn)\n    }\n\n    fun generateAdminAccessToken(id: Long): String {\n        val issuedAt = Date()\n        val accessTokenExpiresIn = Date(issuedAt.time + jwtProperties.accessExp * MILLI_TO_SECOND)\n        return buildAccessToken(id, issuedAt, accessTokenExpiresIn, audience = ADMIN_AUDIENCE)\n    }\n\n    fun generateRefreshToken(id: Long): String {\n        val issuedAt = Date()\n        val refreshTokenExpiresIn = Date(issuedAt.time + jwtProperties.refreshExp * MILLI_TO_SECOND)\n        return buildRefreshToken(id, issuedAt, refreshTokenExpiresIn)\n    }\n\n    fun isAccessToken(token: String): Boolean =\n        getJws(token).payload.get(TOKEN_TYPE) == ACCESS_TOKEN\n\n    fun isRefreshToken(token: String): Boolean =\n        getJws(token).payload.get(TOKEN_TYPE) == REFRESH_TOKEN\n\n    fun parseAccessToken(token: String): AccessTokenInfo {\n        if (isAccessToken(token)) {\n            val claims = getJws(token).payload\n            val audiences = claims.audience\n            return AccessTokenInfo(\n                userId = claims.subject.toLong(),\n                isAdmin = audiences != null && ADMIN_AUDIENCE in audiences,\n            )\n        }\n        throw InvalidTokenException.EXCEPTION\n    }\n\n    fun parseRefreshToken(token: String): Long {\n        try {\n            if (isRefreshToken(token)) {\n                val claims = getJws(token).payload\n                return claims.subject.toLong()\n            }\n        } catch (e: ExpiredTokenException) {\n            throw RefreshTokenExpiredException.EXCEPTION\n        }\n        throw InvalidTokenException.EXCEPTION\n    }\n\n    fun getRefreshTokenTTlSecond(): Long = jwtProperties.refreshExp\n\n    fun getAccessTokenTTlSecond(): Long = jwtProperties.accessExp\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/properties/JwtProperties.kt",
    "content": "package band.gosrock.common.properties\n\nimport org.springframework.boot.context.properties.ConfigurationProperties\n\n@ConfigurationProperties(prefix = \"auth.jwt\")\ndata class JwtProperties(\n    val secretKey: String,\n    val accessExp: Long,\n    val refreshExp: Long,\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/properties/OauthProperties.kt",
    "content": "package band.gosrock.common.properties\n\nimport org.springframework.boot.context.properties.ConfigurationProperties\n\n@ConfigurationProperties(\"oauth\")\nclass OauthProperties(\n    private val kakao: OAuthSecret,\n) {\n    fun getKakaoAdminKey(): String = kakao.adminKey\n    fun getKakaoBaseUrl(): String = kakao.baseUrl\n    fun getKakaoClientId(): String = kakao.clientId\n    fun getKakaoRedirectUrl(): String = kakao.redirectUrl\n    fun getKakaoClientSecret(): String = kakao.clientSecret\n    fun getKakaoAppId(): String = kakao.appId\n\n    data class OAuthSecret(\n        var baseUrl: String = \"\",\n        var clientId: String = \"\",\n        var clientSecret: String = \"\",\n        var redirectUrl: String = \"\",\n        var appId: String = \"\",\n        var adminKey: String = \"\",\n    )\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/properties/TossPaymentsProperties.kt",
    "content": "package band.gosrock.common.properties\n\nimport org.springframework.boot.context.properties.ConfigurationProperties\n\n@ConfigurationProperties(\"toss\")\ndata class TossPaymentsProperties(\n    val secretKey: String,\n    val mid: String,\n)\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/validator/EnumValidator.kt",
    "content": "package band.gosrock.common.validator\n\nimport band.gosrock.common.annotation.Enum as EnumAnnotation\nimport jakarta.validation.ConstraintValidator\nimport jakarta.validation.ConstraintValidatorContext\n\nclass EnumValidator : ConstraintValidator<EnumAnnotation, kotlin.Enum<*>> {\n    override fun isValid(value: kotlin.Enum<*>?, context: ConstraintValidatorContext): Boolean {\n        if (value == null) return false\n        val reflectionEnumClass = value.declaringJavaClass\n        return reflectionEnumClass.enumConstants.contains(value)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/kotlin/band/gosrock/common/validator/PhoneValidator.kt",
    "content": "package band.gosrock.common.validator\n\nimport band.gosrock.common.annotation.Phone\nimport jakarta.validation.ConstraintValidator\nimport jakarta.validation.ConstraintValidatorContext\n\nclass PhoneValidator : ConstraintValidator<Phone, String> {\n    override fun isValid(value: String?, context: ConstraintValidatorContext): Boolean {\n        if (value == null) return false\n        return value.matches(Regex(\"^01(?:0|1|[6-9])-(?:\\\\d{3}|\\\\d{4})-\\\\d{4}$\"))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/main/resources/application-common-local.yml",
    "content": "auth:\n  jwt:\n    secret-key: testkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkey\n    access-exp: 86400\n    refresh-exp: 604800\n\noauth:\n  kakao:\n    base-url: https://kauth.kakao.com\n    client-id: DUMMY_LOCAL_CLIENT\n    client-secret: DUMMY_LOCAL_SECRET\n    redirect-url: http://localhost:8080/api/v1/auth/oauth/kakao/develop\n    app-id: DUMMY_LOCAL_APP_ID\n    admin-key: DUMMY_LOCAL_ADMIN_KEY\n\ntoss:\n  secret-key: test_sk_ADpexMgkW36weAqp4bNVGbR5ozO0\n  mid: gosroc9mwo\n"
  },
  {
    "path": "DuDoong-Common/src/main/resources/application-common.yml",
    "content": "auth:\n  jwt:\n    secret-key: ${JWT_SECRET_KEY:testkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkey}\n    access-exp: ${JWT_ACCESS_EXP:3600}\n    refresh-exp: ${JWT_REFRESH_EXP:3600}\n\noauth:\n  kakao:\n    base-url: ${KAKAO_BASE_URL}\n    client-id: ${KAKAO_CLIENT}\n    client-secret: ${KAKAO_SECRET}\n    redirect-url: ${KAKAO_REDIRECT}\n    app-id: ${KAKAO_APP_ID}\n    admin-key: ${KAKAO_ADMIN_KEY}\n\ntoss:\n  secret-key: ${TOSS_PAYMENTS_KEY:test_sk_ADpexMgkW36weAqp4bNVGbR5ozO0}\n  mid : ${TOSS_MID:gosroc9mwo}"
  },
  {
    "path": "DuDoong-Common/src/test/kotlin/band/gosrock/common/jwt/JwtTokenProviderTest.kt",
    "content": "package band.gosrock.common.jwt\n\nimport band.gosrock.common.consts.DuDoongStatic\nimport band.gosrock.common.properties.JwtProperties\nimport io.jsonwebtoken.Jwts\nimport io.jsonwebtoken.security.Keys\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Assertions.assertFalse\nimport org.junit.jupiter.api.Assertions.assertNotNull\nimport org.junit.jupiter.api.Assertions.assertTrue\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.DisplayName\nimport org.junit.jupiter.api.Nested\nimport org.junit.jupiter.api.Test\nimport java.nio.charset.StandardCharsets\n\nclass JwtTokenProviderTest {\n\n    private lateinit var jwtTokenProvider: JwtTokenProvider\n    private val secretKey = \"testkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkey\"\n    private val accessExp = 3600L\n    private val refreshExp = 7200L\n\n    @BeforeEach\n    fun setUp() {\n        val props = JwtProperties(\n            secretKey = secretKey,\n            accessExp = accessExp,\n            refreshExp = refreshExp,\n        )\n        jwtTokenProvider = JwtTokenProvider(props)\n    }\n\n    private fun parseClaims(token: String) =\n        Jwts.parser()\n            .verifyWith(Keys.hmacShaKeyFor(secretKey.toByteArray(StandardCharsets.UTF_8)))\n            .build()\n            .parseSignedClaims(token)\n            .payload\n\n    @Nested\n    @DisplayName(\"일반 Access Token\")\n    inner class GeneralAccessToken {\n\n        @Test\n        @DisplayName(\"JWT payload에 role 클레임이 포함되지 않는다\")\n        fun `access token does not contain role claim`() {\n            val token = jwtTokenProvider.generateAccessToken(1L)\n            val claims = parseClaims(token)\n\n            assertFalse(claims.containsKey(\"role\"), \"JWT에 role 클레임이 포함되어서는 안 됩니다\")\n        }\n\n        @Test\n        @DisplayName(\"JWT payload에 userId(subject)만 포함된다\")\n        fun `access token contains only userId as subject`() {\n            val token = jwtTokenProvider.generateAccessToken(42L)\n            val claims = parseClaims(token)\n\n            assertEquals(\"42\", claims.subject)\n            assertEquals(DuDoongStatic.ACCESS_TOKEN, claims[DuDoongStatic.TOKEN_TYPE])\n            assertEquals(DuDoongStatic.TOKEN_ISSUER, claims.issuer)\n        }\n\n        @Test\n        @DisplayName(\"일반 토큰의 audience에 admin이 없다\")\n        fun `general access token has no admin audience`() {\n            val token = jwtTokenProvider.generateAccessToken(1L)\n            val claims = parseClaims(token)\n\n            val audiences = claims.audience\n            assertTrue(audiences == null || DuDoongStatic.ADMIN_AUDIENCE !in audiences,\n                \"일반 토큰에 admin audience가 포함되어서는 안 됩니다\")\n        }\n\n        @Test\n        @DisplayName(\"parseAccessToken 시 isAdmin이 false이다\")\n        fun `parseAccessToken returns isAdmin false for general token`() {\n            val token = jwtTokenProvider.generateAccessToken(10L)\n            val info = jwtTokenProvider.parseAccessToken(token)\n\n            assertEquals(10L, info.userId)\n            assertFalse(info.isAdmin, \"일반 토큰의 isAdmin은 false여야 합니다\")\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Admin Access Token\")\n    inner class AdminAccessToken {\n\n        @Test\n        @DisplayName(\"Admin 토큰에 audience:admin 클레임이 포함된다\")\n        fun `admin access token contains admin audience`() {\n            val token = jwtTokenProvider.generateAdminAccessToken(1L)\n            val claims = parseClaims(token)\n\n            val audiences = claims.audience\n            assertNotNull(audiences, \"Admin 토큰에 audience가 있어야 합니다\")\n            assertTrue(DuDoongStatic.ADMIN_AUDIENCE in audiences!!,\n                \"Admin 토큰에 'admin' audience가 포함되어야 합니다\")\n        }\n\n        @Test\n        @DisplayName(\"Admin 토큰에도 role 클레임이 없다\")\n        fun `admin access token does not contain role claim`() {\n            val token = jwtTokenProvider.generateAdminAccessToken(1L)\n            val claims = parseClaims(token)\n\n            assertFalse(claims.containsKey(\"role\"), \"Admin JWT에도 role 클레임이 포함되어서는 안 됩니다\")\n        }\n\n        @Test\n        @DisplayName(\"parseAccessToken 시 isAdmin이 true이다\")\n        fun `parseAccessToken returns isAdmin true for admin token`() {\n            val token = jwtTokenProvider.generateAdminAccessToken(99L)\n            val info = jwtTokenProvider.parseAccessToken(token)\n\n            assertEquals(99L, info.userId)\n            assertTrue(info.isAdmin, \"Admin 토큰의 isAdmin은 true여야 합니다\")\n        }\n\n        @Test\n        @DisplayName(\"Admin 토큰과 일반 토큰의 userId는 동일하게 파싱된다\")\n        fun `admin and general tokens parse same userId`() {\n            val generalToken = jwtTokenProvider.generateAccessToken(55L)\n            val adminToken = jwtTokenProvider.generateAdminAccessToken(55L)\n\n            val generalInfo = jwtTokenProvider.parseAccessToken(generalToken)\n            val adminInfo = jwtTokenProvider.parseAccessToken(adminToken)\n\n            assertEquals(generalInfo.userId, adminInfo.userId)\n            assertFalse(generalInfo.isAdmin)\n            assertTrue(adminInfo.isAdmin)\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Refresh Token\")\n    inner class RefreshToken {\n\n        @Test\n        @DisplayName(\"Refresh 토큰은 userId만 포함한다\")\n        fun `refresh token contains only userId`() {\n            val token = jwtTokenProvider.generateRefreshToken(7L)\n            val claims = parseClaims(token)\n\n            assertEquals(\"7\", claims.subject)\n            assertEquals(DuDoongStatic.REFRESH_TOKEN, claims[DuDoongStatic.TOKEN_TYPE])\n            assertFalse(claims.containsKey(\"role\"))\n        }\n\n        @Test\n        @DisplayName(\"parseRefreshToken으로 userId를 추출할 수 있다\")\n        fun `parseRefreshToken extracts userId`() {\n            val token = jwtTokenProvider.generateRefreshToken(33L)\n            val userId = jwtTokenProvider.parseRefreshToken(token)\n\n            assertEquals(33L, userId)\n        }\n    }\n\n    @Nested\n    @DisplayName(\"토큰 구분\")\n    inner class TokenTypeDistinction {\n\n        @Test\n        @DisplayName(\"Access Token은 isAccessToken=true, isRefreshToken=false\")\n        fun `access token type check`() {\n            val token = jwtTokenProvider.generateAccessToken(1L)\n\n            assertTrue(jwtTokenProvider.isAccessToken(token))\n            assertFalse(jwtTokenProvider.isRefreshToken(token))\n        }\n\n        @Test\n        @DisplayName(\"Admin Access Token도 isAccessToken=true\")\n        fun `admin access token is also access token`() {\n            val token = jwtTokenProvider.generateAdminAccessToken(1L)\n\n            assertTrue(jwtTokenProvider.isAccessToken(token))\n            assertFalse(jwtTokenProvider.isRefreshToken(token))\n        }\n\n        @Test\n        @DisplayName(\"Refresh Token은 isRefreshToken=true, isAccessToken=false\")\n        fun `refresh token type check`() {\n            val token = jwtTokenProvider.generateRefreshToken(1L)\n\n            assertTrue(jwtTokenProvider.isRefreshToken(token))\n            assertFalse(jwtTokenProvider.isAccessToken(token))\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/test/kotlin/band/gosrock/common/properties/JwtPropertiesTest.kt",
    "content": "package band.gosrock.common.properties\n\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Test\nimport org.springframework.beans.factory.annotation.Autowired\nimport org.springframework.boot.test.context.SpringBootTest\nimport org.springframework.test.context.ActiveProfiles\n\n@SpringBootTest\n@ActiveProfiles(\"common\")\nclass JwtPropertiesTest {\n\n    @Autowired\n    lateinit var jwtProperties: JwtProperties\n\n    @Test\n    fun `JWT 환경변수값이 불러와지는지 확인`() {\n        val accessExp = jwtProperties.accessExp\n        val refreshExp = jwtProperties.refreshExp\n        assertEquals(accessExp, 3600)\n        assertEquals(refreshExp, 3600)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/test/kotlin/band/gosrock/common/properties/TossPaymentsPropertiesTest.kt",
    "content": "package band.gosrock.common.properties\n\nimport org.junit.jupiter.api.Assertions.assertNotNull\nimport org.junit.jupiter.api.Test\nimport org.springframework.beans.factory.annotation.Autowired\nimport org.springframework.boot.test.context.SpringBootTest\n\n@SpringBootTest\n// @ActiveProfiles(\"common\")\nclass TossPaymentsPropertiesTest {\n\n    @Autowired\n    lateinit var tossPaymentsProperties: TossPaymentsProperties\n\n    @Test\n    fun `토스페이먼츠 환경변수 값 확인`() {\n        val secretKey = tossPaymentsProperties.secretKey\n        val mid = tossPaymentsProperties.mid\n        assertNotNull(secretKey)\n        assertNotNull(mid)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Common/src/test/resources/application.yml",
    "content": "auth:\n  jwt:\n    secret-key: ${JWT_SECRET_KEY:testkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkey}\n    access-exp: ${JWT_ACCESS_EXP:3600}\n    refresh-exp: ${JWT_REFRESH_EXP:3600}\n\noauth:\n  kakao:\n    base-url: ${KAKAO_BASE_URL}\n    client-id: ${KAKAO_CLIENT}\n    client-secret: ${KAKAO_SECRET}\n    redirect-url: ${KAKAO_REDIRECT}\n    app-id: ${KAKAO_APP_ID}\n    admin-key: ${KAKAO_ADMIN_KEY}\n\n#테스트 키 입니다 상관없어요\ntoss:\n  secret-key: ${TOSS_PAYMENTS_KEY:test_sk_ADpexMgkW36weAqp4bNVGbR5ozO0}\n  mid : ${TOSS_MID:gosroc9mwo}"
  },
  {
    "path": "DuDoong-Common/src/test/resources/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n  <include resource=\"org/springframework/boot/logging/logback/base.xml\" />\n  <logger name=\"org.springframework\" level=\"INFO\"/>\n  <include resource=\"band.gosrock\"/>\n  <logger name=\"band.gosrock\" level=\"DEBUG\"/>\n</configuration>"
  },
  {
    "path": "DuDoong-Domain/build.gradle.kts",
    "content": "tasks.bootJar { enabled = false }\ntasks.jar { enabled = true }\n\ndependencies {\n    api(\"org.springframework.boot:spring-boot-starter-data-jpa\")\n    api(\"com.mysql:mysql-connector-j\")\n    runtimeOnly(\"com.h2database:h2\")\n    implementation(project(\":DuDoong-Common\"))\n    implementation(project(\":DuDoong-Infrastructure\"))\n\n    // QueryDSL (KAPT - Kotlin/Java 모든 엔티티의 Q클래스 생성)\n    api(\"com.querydsl:querydsl-core\")\n    api(\"com.querydsl:querydsl-jpa:5.0.0:jakarta\")\n    kapt(\"com.querydsl:querydsl-apt:5.0.0:jakarta\")\n    kapt(\"jakarta.persistence:jakarta.persistence-api\")\n    kapt(\"jakarta.annotation:jakarta.annotation-api\")\n\n    // for @Nullable\n    implementation(\"com.google.code.findbugs:jsr305:3.0.2\")\n}\n\n// QueryDSL Q클래스 생성 경로 (KAPT 전환 - src/main/generated 제거)\ntasks.clean {\n    doLast {\n        file(\"src/main/generated\").deleteRecursively()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/DomainPackageLocation.kt",
    "content": "package band.gosrock.domain\n\ninterface DomainPackageLocation\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/DuDoongDomainApplication.kt",
    "content": "package band.gosrock.domain\n\nimport org.springframework.boot.autoconfigure.SpringBootApplication\n\n// 테스팅 용도 어플리케이션입니다.\n@SpringBootApplication\nclass DuDoongDomainApplication\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/alarm/EventSlackAlarm.kt",
    "content": "package band.gosrock.domain.common.alarm\n\nimport band.gosrock.domain.domains.event.domain.Event\n\nobject EventSlackAlarm {\n    @JvmStatic\n    fun creationOf(eventName: String): String = \"새로운 공연 '$eventName'이(가) 열렸습니다!\"\n\n    @JvmStatic\n    fun changeStatusOf(event: Event): String = \"${nameOf(event)}의 상태가 ${statusOf(event)}으로 변경되었습니다.\"\n\n    @JvmStatic\n    fun changeContentOf(event: Event): String = \"${nameOf(event)}의 내용이 변경되었습니다. 확인해주세요!\"\n\n    @JvmStatic\n    fun deletionOf(eventName: String): String = \"'$eventName' 공연이 삭제되었습니다.\"\n\n    private fun nameOf(event: Event): String = \"'${event.eventBasic?.name}'\"\n\n    private fun statusOf(event: Event): String = \"'${event.status?.value}'\"\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/alarm/HostSlackAlarm.kt",
    "content": "package band.gosrock.domain.common.alarm\n\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.host.domain.Host\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.user.domain.User\n\nobject HostSlackAlarm {\n    @JvmStatic\n    fun joinOf(host: Host, user: User): String = \"${nameOf(user)}님이 ${nameOf(host)}에 가입했습니다!\"\n\n    @JvmStatic\n    fun slackRegistrationOf(host: Host): String = \"${nameOf(host)}에 슬랙 알림이 등록되었습니다! 환영합니다\"\n\n    @JvmStatic\n    fun changeMasterOf(host: Host, user: User): String =\n        \"${nameOf(host)}의 마스터가 ${nameOf(user)}님으로 변경되었습니다.\"\n\n    @JvmStatic\n    fun disabledOf(user: User): String = \"${nameOf(user)}님이 호스트에서 추방당했습니다.\"\n\n    @JvmStatic\n    fun newConfirmOrder(event: Event, order: Order): String =\n        getEventOrderTitle(event) + getOrderNo(order) + \" 주문이 결제되었습니다.\\n\" + getOrderNameAndAmount(order)\n\n    @JvmStatic\n    fun newApproveOrder(event: Event, order: Order): String =\n        getEventOrderTitle(event) + getOrderNo(order) + \" 주문 승인이 요청 되었습니다.\\n\" + getOrderNameAndAmount(order)\n\n    @JvmStatic\n    fun approvedOrder(event: Event, order: Order): String =\n        getEventOrderTitle(event) + getOrderNo(order) + \" 주문이 승인 되었습니다.\\n\" + getOrderNameAndAmount(order)\n\n    @JvmStatic\n    fun withDrawOrder(event: Event, order: Order): String =\n        getEventOrderTitle(event) + getOrderNo(order) + \" 주문이 철회 되었습니다.\\n\" + getOrderNameAndAmount(order)\n\n    @JvmStatic\n    fun dudoongOrderRefund(event: Event, order: Order): String =\n        getEventOrderTitle(event) +\n            getOrderNo(order) +\n            \" 두둥티켓 주문이 구매자에의해 환불 처리 되었습니다. 구매자에게 연락해서 환불을 진행해 주세요.\\n\" +\n            getOrderNameAndAmount(order)\n\n    @JvmStatic\n    fun dudoongOrderCancel(event: Event, order: Order): String =\n        getEventOrderTitle(event) +\n            getOrderNo(order) +\n            \" 두둥티켓 주문이 관리자에의해 환불 처리 되었습니다. 구매자에게 연락해서 환불을 진행해 주세요.\\n\" +\n            getOrderNameAndAmount(order)\n\n    private fun getEventOrderTitle(event: Event): String =\n        \"${event.eventBasic?.name} 이벤트 주문관련 알림\\n\"\n\n    private fun getOrderNo(order: Order): String = \"주문번호 : ${order.orderNo}\"\n\n    private fun getOrderNameAndAmount(order: Order): String =\n        \"주문이름 :${order.orderName} | 주문 금액 : ${order.getTotalPaymentPrice()}\"\n\n    private fun nameOf(host: Host): String = \"'${host.profile?.name}'\"\n\n    private fun nameOf(user: User): String = \"'${user.profile?.name}'\"\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/alarm/OrderKakaoTalkAlarm.kt",
    "content": "package band.gosrock.domain.common.alarm\n\nobject OrderKakaoTalkAlarm {\n    @JvmStatic\n    fun creationOf(userName: String, hostName: String, eventName: String): String =\n        \"안녕하세요 ${userName}님!\\n\" +\n            \"$hostName $eventName\" +\n            \"티켓이 발급됐습니다.\\n\" +\n            \"\\n\" +\n            \"원활한 입장을 위해 QR코드를 미리 준비해주세요.\"\n\n    @JvmStatic\n    fun creationHeaderOf(): String = \"주문 완료 안내\"\n\n    @JvmStatic\n    fun creationTemplateCode(): String = \"doneorderv2\"\n\n    @JvmStatic\n    fun deletionOf(userName: String, hostName: String, eventName: String): String =\n        \"안녕하세요 ${userName}님!\\n$hostName $eventName 주문이 취소되어 안내드립니다.\\n\"\n\n    @JvmStatic\n    fun deletionHeaderOf(): String = \"주문 취소 안내\"\n\n    @JvmStatic\n    fun deletionTemplateCode(): String = \"cancelorder\"\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/alarm/SettlementKakaoTalkAlarm.kt",
    "content": "package band.gosrock.domain.common.alarm\n\nobject SettlementKakaoTalkAlarm {\n    @JvmStatic\n    fun creationOf(hostName: String): String =\n        \"안녕하세요 ${hostName}님!\\n이메일로 정산서 발송이 완료되어 안내드립니다.\"\n\n    @JvmStatic\n    fun creationHeaderOf(): String = \"정산서 이메일 발송 안내\"\n\n    @JvmStatic\n    fun creationTemplateCode(): String = \"settlement\"\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/alarm/UserKakaoTalkAlarm.kt",
    "content": "package band.gosrock.domain.common.alarm\n\nobject UserKakaoTalkAlarm {\n    @JvmStatic\n    fun creationOf(userName: String): String =\n        userName +\n            \"님, 두둥에 가입하신 것을 환영합니다!\\n\" +\n            \"\\n\" +\n            \"두둥은 누구나 자유롭게 공연을 홍보하고 인원을 모집할 수 있는 서비스입니다.\\n\" +\n            \"호스트가 되어 밴드, 뮤지컬, 버스킹등의 공연을 열고 홍보하세요!\\n\" +\n            \"\\n\"\n\n    @JvmStatic\n    fun creationTemplateCode(): String = \"signup\"\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/aop/domainEvent/DomainEvent.kt",
    "content": "package band.gosrock.domain.common.aop.domainEvent\n\nimport java.time.LocalDateTime\n\nopen class DomainEvent {\n    val publishAt: LocalDateTime = LocalDateTime.now()\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/aop/domainEvent/EventPublisherAspect.kt",
    "content": "package band.gosrock.domain.common.aop.domainEvent\n\nimport org.aspectj.lang.ProceedingJoinPoint\nimport org.aspectj.lang.annotation.Around\nimport org.aspectj.lang.annotation.Aspect\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnExpression\nimport org.springframework.context.ApplicationEventPublisher\nimport org.springframework.context.ApplicationEventPublisherAware\nimport org.springframework.stereotype.Component\n\n@Aspect\n@Component\n@ConditionalOnExpression(\"\\${ableDomainEvent:true}\")\nclass EventPublisherAspect : ApplicationEventPublisherAware {\n\n    private lateinit var publisher: ApplicationEventPublisher\n    private val appliedLocal = ThreadLocal<Boolean>()\n\n    @Around(\"@annotation(org.springframework.transaction.annotation.Transactional)\")\n    @Throws(Throwable::class)\n    fun handleEvent(joinPoint: ProceedingJoinPoint): Any? {\n        val appliedValue = appliedLocal.get()\n        val nested = appliedValue != null && appliedValue\n\n        if (!nested) {\n            appliedLocal.set(true)\n            Events.setPublisher(publisher)\n        }\n\n        return try {\n            joinPoint.proceed()\n        } finally {\n            if (!nested) {\n                Events.reset()\n                appliedLocal.remove()\n            }\n        }\n    }\n\n    override fun setApplicationEventPublisher(eventPublisher: ApplicationEventPublisher) {\n        this.publisher = eventPublisher\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/aop/domainEvent/Events.kt",
    "content": "package band.gosrock.domain.common.aop.domainEvent\n\nimport org.springframework.context.ApplicationEventPublisher\n\nobject Events {\n    private val publisherLocal = ThreadLocal<ApplicationEventPublisher>()\n\n    @JvmStatic\n    fun raise(event: DomainEvent?) {\n        if (event == null) return\n        publisherLocal.get()?.publishEvent(event)\n    }\n\n    @JvmStatic\n    fun setPublisher(publisher: ApplicationEventPublisher) {\n        publisherLocal.set(publisher)\n    }\n\n    @JvmStatic\n    fun reset() {\n        publisherLocal.remove()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/aop/redissonLock/CallTransaction.kt",
    "content": "package band.gosrock.domain.common.aop.redissonLock\n\nimport org.aspectj.lang.ProceedingJoinPoint\n\ninterface CallTransaction {\n    @Throws(Throwable::class)\n    fun proceed(joinPoint: ProceedingJoinPoint): Any?\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/aop/redissonLock/CallTransactionFactory.kt",
    "content": "package band.gosrock.domain.common.aop.redissonLock\n\nimport org.springframework.stereotype.Component\n\n@Component\nclass CallTransactionFactory(\n    private val redissonCallSameTransaction: RedissonCallSameTransaction,\n    private val redissonCallNewTransaction: RedissonCallNewTransaction,\n) {\n    fun getCallTransaction(needSame: Boolean): CallTransaction {\n        return if (needSame) redissonCallSameTransaction else redissonCallNewTransaction\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/aop/redissonLock/RedissonCallNewTransaction.kt",
    "content": "package band.gosrock.domain.common.aop.redissonLock\n\nimport org.aspectj.lang.ProceedingJoinPoint\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.annotation.Propagation\nimport org.springframework.transaction.annotation.Transactional\n\n@Component\nclass RedissonCallNewTransaction : CallTransaction {\n    // 다른 트랜젹선이 걸린서비스가 해당 조인포인트(메서드를) 호출하더라도\n    // 새로운 트랜잭션이 보장되어야합니다. (재고를 감소시키는 로직이므로)\n    // leaseTime 보다 트랜잭션 타임아웃을 작게 설정\n    // leastTimeOut 발생전에 rollback 시키기 위함\n    @Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 9)\n    @Throws(Throwable::class)\n    override fun proceed(joinPoint: ProceedingJoinPoint): Any? {\n        return joinPoint.proceed()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/aop/redissonLock/RedissonCallSameTransaction.kt",
    "content": "package band.gosrock.domain.common.aop.redissonLock\n\nimport org.aspectj.lang.ProceedingJoinPoint\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.annotation.Propagation\nimport org.springframework.transaction.annotation.Transactional\n\n@Component\nclass RedissonCallSameTransaction : CallTransaction {\n    // 다른 트랜젹선이 걸린서비스가 해당 조인포인트(메서드를) 호출하더라도\n    // 새로운 트랜잭션이 보장되어야합니다. (재고를 감소시키는 로직이므로)\n    // leaseTime 보다 트랜잭션 타임아웃을 작게 설정\n    // leastTimeOut 발생전에 rollback 시키기 위함\n    @Transactional(propagation = Propagation.MANDATORY, timeout = 9)\n    @Throws(Throwable::class)\n    override fun proceed(joinPoint: ProceedingJoinPoint): Any? {\n        return joinPoint.proceed()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/aop/redissonLock/RedissonLock.kt",
    "content": "package band.gosrock.domain.common.aop.redissonLock\n\nimport java.util.concurrent.TimeUnit\nimport kotlin.reflect.KClass\n\n/** Redisson 을 활용한 분산락을 걸 메소드에 다는 어노테이션 입니다. */\n@Target(AnnotationTarget.FUNCTION)\n@Retention(AnnotationRetention.RUNTIME)\nannotation class RedissonLock(\n    // 분산락을 걸 파라미터 네임\n    val identifier: String,\n    // 락 이름\n    val LockName: String,\n    val paramClassType: KClass<*> = Any::class,\n    val needSameTransaction: Boolean = false,\n    // redisson default waitTime 이 30 s 임\n    val waitTime: Long = 10L,\n    val leaseTime: Long = 10L,\n    // 초단위 계산\n    val timeUnit: TimeUnit = TimeUnit.SECONDS,\n)\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/aop/redissonLock/RedissonLockAop.kt",
    "content": "package band.gosrock.domain.common.aop.redissonLock\n\nimport band.gosrock.common.exception.BadLockIdentifierException\nimport band.gosrock.common.exception.DuDoongCodeException\nimport band.gosrock.common.exception.DuDoongDynamicException\nimport band.gosrock.common.exception.NotAvailableRedissonLockException\nimport org.aspectj.lang.ProceedingJoinPoint\nimport org.aspectj.lang.annotation.Around\nimport org.aspectj.lang.annotation.Aspect\nimport org.aspectj.lang.reflect.MethodSignature\nimport org.redisson.api.RedissonClient\nimport org.slf4j.LoggerFactory\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnExpression\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.TransactionTimedOutException\nimport org.springframework.util.StringUtils\nimport java.lang.reflect.InvocationTargetException\nimport java.util.concurrent.TimeUnit\n\n@Aspect\n@Component\n@ConditionalOnExpression(\"\\${ableRedissonLock:true}\")\nclass RedissonLockAop(\n    private val redissonClient: RedissonClient,\n    private val callTransactionFactory: CallTransactionFactory,\n) {\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    @Around(\"@annotation(band.gosrock.domain.common.aop.redissonLock.RedissonLock)\")\n    @Throws(Throwable::class)\n    fun lock(joinPoint: ProceedingJoinPoint): Any? {\n        val signature = joinPoint.signature as MethodSignature\n        val method = signature.method\n        val redissonLock = method.getAnnotation(RedissonLock::class.java)\n\n        val baseKey = redissonLock.LockName\n        val dynamicKey = generateDynamicKey(\n            redissonLock.identifier,\n            joinPoint.args,\n            redissonLock.paramClassType.java,  // KClass -> Class\n            signature.parameterNames,\n        )\n\n        val rLock = redissonClient.getLock(\"$baseKey:$dynamicKey\")\n        log.info(\"redisson 키 설정$baseKey:$dynamicKey\")\n\n        val waitTime = redissonLock.waitTime\n        val leaseTime = redissonLock.leaseTime\n        val timeUnit: TimeUnit = redissonLock.timeUnit\n\n        try {\n            val available = rLock.tryLock(waitTime, leaseTime, timeUnit)\n            if (!available) throw NotAvailableRedissonLockException.EXCEPTION\n\n            log.info(\"redisson 락 안으로 진입 $baseKey:$dynamicKey 쓰레드 아이디${Thread.currentThread().id}\")\n            return callTransactionFactory.getCallTransaction(redissonLock.needSameTransaction).proceed(joinPoint)\n        } catch (e: DuDoongCodeException) {\n            throw e\n        } catch (e: DuDoongDynamicException) {\n            throw e\n        } catch (e: TransactionTimedOutException) {\n            throw e\n        } finally {\n            try {\n                rLock.unlock()\n            } catch (e: IllegalMonitorStateException) {\n                log.error(\"$e$baseKey$dynamicKey\")\n                throw e\n            }\n        }\n    }\n\n    fun generateDynamicKey(\n        identifier: String,\n        args: Array<Any>,\n        paramClassType: Class<*>,\n        parameterNames: Array<String>,\n    ): String {\n        return try {\n            if (paramClassType == Any::class.java) {\n                createDynamicKeyFromPrimitive(parameterNames, args, identifier)\n            } else {\n                createDynamicKeyFromObject(args, paramClassType, identifier)\n            }\n        } catch (e: IllegalAccessException) {\n            log.error(e.message)\n            throw BadLockIdentifierException.EXCEPTION\n        } catch (e: NoSuchMethodException) {\n            log.error(e.message)\n            throw BadLockIdentifierException.EXCEPTION\n        } catch (e: InvocationTargetException) {\n            log.error(e.message)\n            throw BadLockIdentifierException.EXCEPTION\n        }\n    }\n\n    fun createDynamicKeyFromPrimitive(\n        methodParameterNames: Array<String>,\n        args: Array<Any>,\n        paramName: String,\n    ): String {\n        for (i in methodParameterNames.indices) {\n            if (methodParameterNames[i] == paramName) {\n                return args[i].toString()\n            }\n        }\n        throw BadLockIdentifierException.EXCEPTION\n    }\n\n    @Throws(IllegalAccessException::class, NoSuchMethodException::class, InvocationTargetException::class)\n    fun createDynamicKeyFromObject(\n        args: Array<Any>,\n        paramClassType: Class<*>,\n        identifier: String,\n    ): String {\n        val paramClassName = paramClassType.simpleName\n        for (arg in args) {\n            val argsClassName = arg.javaClass.simpleName\n            if (argsClassName.startsWith(paramClassName)) {\n                val aClass = arg.javaClass\n                val capitalize = StringUtils.capitalize(identifier)\n                val result = aClass.getMethod(\"get$capitalize\").invoke(arg)\n                return result.toString()\n            }\n        }\n        throw BadLockIdentifierException.EXCEPTION\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/converter/BigDecimalScale6WithBankersRoundingConverter.kt",
    "content": "package band.gosrock.domain.common.converter\n\nimport java.math.BigDecimal\nimport java.math.RoundingMode\nimport jakarta.persistence.AttributeConverter\n\nclass BigDecimalScale6WithBankersRoundingConverter : AttributeConverter<BigDecimal, String> {\n\n    override fun convertToDatabaseColumn(attribute: BigDecimal?): String {\n        if (attribute == null) return BigDecimal.ZERO.toString()\n        return attribute.setScale(6, RoundingMode.HALF_EVEN).toString()\n    }\n\n    override fun convertToEntityAttribute(dbData: String?): BigDecimal {\n        if (dbData == null) return BigDecimal.ZERO\n        return BigDecimal(dbData)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/dto/ProfileViewDto.kt",
    "content": "package band.gosrock.domain.common.dto\n\nimport band.gosrock.domain.common.vo.ImageVo\nimport band.gosrock.domain.domains.user.domain.User\n\nclass ProfileViewDto(\n    val id: Long?,\n    val profileImage: ImageVo?,\n    val name: String?,\n) {\n    companion object {\n        @JvmStatic\n        fun from(user: User): ProfileViewDto =\n            ProfileViewDto(\n                id = user.id,\n                name = user.profile?.name,\n                profileImage = user.profile?.profileImage,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/events/event/EventContentChangeEvent.kt",
    "content": "package band.gosrock.domain.common.events.event\n\nimport band.gosrock.domain.common.aop.domainEvent.DomainEvent\nimport band.gosrock.domain.domains.event.domain.Event\n\ndata class EventContentChangeEvent(\n    val hostId: Long,\n    val eventId: Long,\n) : DomainEvent() {\n    companion object {\n        @JvmStatic\n        fun of(event: Event): EventContentChangeEvent =\n            EventContentChangeEvent(\n                hostId = event.hostId ?: 0L,\n                eventId = event.id ?: 0L,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/events/event/EventCreationEvent.kt",
    "content": "package band.gosrock.domain.common.events.event\n\nimport band.gosrock.domain.common.aop.domainEvent.DomainEvent\n\ndata class EventCreationEvent(\n    val hostId: Long?,\n    val eventName: String?,\n) : DomainEvent() {\n    companion object {\n        @JvmStatic\n        fun of(hostId: Long?, eventName: String?): EventCreationEvent =\n            EventCreationEvent(hostId = hostId, eventName = eventName)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/events/event/EventDeletionEvent.kt",
    "content": "package band.gosrock.domain.common.events.event\n\nimport band.gosrock.domain.common.aop.domainEvent.DomainEvent\nimport band.gosrock.domain.domains.event.domain.Event\n\ndata class EventDeletionEvent(\n    val hostId: Long,\n    val eventName: String,\n) : DomainEvent() {\n    companion object {\n        @JvmStatic\n        fun of(event: Event): EventDeletionEvent =\n            EventDeletionEvent(\n                hostId = event.hostId ?: 0L,\n                eventName = event.eventBasic?.name ?: \"\",\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/events/event/EventStatusChangeEvent.kt",
    "content": "package band.gosrock.domain.common.events.event\n\nimport band.gosrock.domain.common.aop.domainEvent.DomainEvent\nimport band.gosrock.domain.domains.event.domain.Event\n\ndata class EventStatusChangeEvent(\n    val hostId: Long,\n    val eventId: Long,\n) : DomainEvent() {\n    companion object {\n        @JvmStatic\n        fun of(event: Event): EventStatusChangeEvent =\n            EventStatusChangeEvent(\n                hostId = event.hostId ?: 0L,\n                eventId = event.id ?: 0L,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/events/host/HostRegisterSlackEvent.kt",
    "content": "package band.gosrock.domain.common.events.host\n\nimport band.gosrock.domain.common.aop.domainEvent.DomainEvent\nimport band.gosrock.domain.domains.host.domain.Host\n\nclass HostRegisterSlackEvent(\n    val hostId: Long?,\n    val hostName: String?,\n) : DomainEvent() {\n    companion object {\n        @JvmStatic\n        fun of(host: Host): HostRegisterSlackEvent =\n            HostRegisterSlackEvent(\n                hostId = host.id,\n                hostName = host.toHostProfileVo().name,\n            )\n    }\n\n    override fun toString(): String = \"HostRegisterSlackEvent(hostId=$hostId, hostName=$hostName)\"\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/events/host/HostUserDisabledEvent.kt",
    "content": "package band.gosrock.domain.common.events.host\n\nimport band.gosrock.domain.common.aop.domainEvent.DomainEvent\nimport band.gosrock.domain.domains.host.domain.Host\nimport band.gosrock.domain.domains.host.domain.HostUser\n\nclass HostUserDisabledEvent(\n    val hostId: Long?,\n    val hostName: String?,\n    val userId: Long?,\n) : DomainEvent() {\n    companion object {\n        @JvmStatic\n        fun of(host: Host, hostUser: HostUser): HostUserDisabledEvent =\n            HostUserDisabledEvent(\n                hostId = host.id,\n                hostName = host.toHostProfileVo().name,\n                userId = hostUser.userId,\n            )\n    }\n\n    override fun toString(): String = \"HostUserDisabledEvent(hostId=$hostId, userId=$userId)\"\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/events/host/HostUserInvitationEvent.kt",
    "content": "package band.gosrock.domain.common.events.host\n\nimport band.gosrock.domain.common.aop.domainEvent.DomainEvent\nimport band.gosrock.domain.common.vo.HostProfileVo\nimport band.gosrock.domain.domains.host.domain.Host\nimport band.gosrock.domain.domains.host.domain.HostRole\nimport band.gosrock.domain.domains.host.domain.HostUser\n\nclass HostUserInvitationEvent(\n    val userId: Long?,\n    val role: HostRole?,\n    val hostProfileVo: HostProfileVo?,\n) : DomainEvent() {\n    companion object {\n        @JvmStatic\n        fun of(host: Host, hostUser: HostUser): HostUserInvitationEvent =\n            HostUserInvitationEvent(\n                hostProfileVo = host.toHostProfileVo(),\n                role = hostUser.role,\n                userId = hostUser.userId,\n            )\n    }\n\n    override fun toString(): String = \"HostUserInvitationEvent(userId=$userId, role=$role)\"\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/events/host/HostUserJoinEvent.kt",
    "content": "package band.gosrock.domain.common.events.host\n\nimport band.gosrock.domain.common.aop.domainEvent.DomainEvent\n\nclass HostUserJoinEvent(\n    val hostId: Long?,\n    val userId: Long?,\n) : DomainEvent() {\n    companion object {\n        @JvmStatic\n        fun of(hostId: Long?, userId: Long?): HostUserJoinEvent =\n            HostUserJoinEvent(hostId, userId)\n    }\n\n    override fun toString(): String = \"HostUserJoinEvent(hostId=$hostId, userId=$userId)\"\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/events/host/HostUserRoleChangeEvent.kt",
    "content": "package band.gosrock.domain.common.events.host\n\nimport band.gosrock.domain.common.aop.domainEvent.DomainEvent\nimport band.gosrock.domain.domains.host.domain.Host\nimport band.gosrock.domain.domains.host.domain.HostRole\nimport band.gosrock.domain.domains.host.domain.HostUser\n\nclass HostUserRoleChangeEvent(\n    val hostId: Long?,\n    val hostName: String?,\n    val role: HostRole?,\n    val userId: Long?,\n) : DomainEvent() {\n    companion object {\n        @JvmStatic\n        fun of(host: Host, hostUser: HostUser): HostUserRoleChangeEvent =\n            HostUserRoleChangeEvent(\n                hostId = host.id,\n                hostName = host.toHostProfileVo().name,\n                role = hostUser.role,\n                userId = hostUser.userId,\n            )\n    }\n\n    override fun toString(): String = \"HostUserRoleChangeEvent(hostId=$hostId, role=$role, userId=$userId)\"\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/events/issuedTicket/EntranceIssuedTicketEvent.kt",
    "content": "package band.gosrock.domain.common.events.issuedTicket\n\nimport band.gosrock.domain.common.aop.domainEvent.DomainEvent\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketUserInfoVo\n\ndata class EntranceIssuedTicketEvent(\n    val issuedTicketNo: String,\n    val eventId: Long,\n    val userInfo: IssuedTicketUserInfoVo?,\n) : DomainEvent() {\n    companion object {\n        @JvmStatic\n        fun from(issuedTicket: IssuedTicket): EntranceIssuedTicketEvent =\n            EntranceIssuedTicketEvent(\n                eventId = issuedTicket.eventId ?: 0L,\n                issuedTicketNo = issuedTicket.issuedTicketNo ?: \"\",\n                userInfo = issuedTicket.userInfo,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/events/order/CreateOrderEvent.kt",
    "content": "package band.gosrock.domain.common.events.order\n\nimport band.gosrock.domain.common.aop.domainEvent.DomainEvent\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderMethod\n\nclass CreateOrderEvent private constructor(\n    val orderUuid: String,\n    val userId: Long,\n    @get:JvmName(\"getIsUsingCoupon\") val isUsingCoupon: Boolean,\n    val orderMethod: OrderMethod,\n    val issuedCouponId: Long?,\n) : DomainEvent() {\n    companion object {\n        @JvmStatic\n        fun from(order: Order): CreateOrderEvent = CreateOrderEvent(\n            userId = order.userId!!,\n            orderUuid = order.uuid!!,\n            isUsingCoupon = order.hasCoupon(),\n            issuedCouponId = order.orderCouponVo.couponId,\n            orderMethod = order.orderMethod!!,\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/events/order/DoneOrderEvent.kt",
    "content": "package band.gosrock.domain.common.events.order\n\nimport band.gosrock.domain.common.aop.domainEvent.DomainEvent\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderMethod\n\nclass DoneOrderEvent private constructor(\n    val orderUuid: String,\n    val userId: Long,\n    val orderMethod: OrderMethod,\n    val paymentKey: String?,\n    val itemId: Long,\n) : DomainEvent() {\n    companion object {\n        @JvmStatic\n        fun from(order: Order): DoneOrderEvent = DoneOrderEvent(\n            orderMethod = order.orderMethod!!,\n            paymentKey = if (order.isNeedPaid()) order.paymentKey else null,\n            userId = order.userId!!,\n            orderUuid = order.uuid!!,\n            itemId = order.itemId,\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/events/order/WithDrawOrderEvent.kt",
    "content": "package band.gosrock.domain.common.events.order\n\nimport band.gosrock.domain.common.aop.domainEvent.DomainEvent\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderMethod\nimport band.gosrock.domain.domains.order.domain.OrderStatus\n\nclass WithDrawOrderEvent private constructor(\n    val orderUuid: String,\n    val userId: Long,\n    val orderMethod: OrderMethod,\n    val orderStatus: OrderStatus,\n    val isDudoongTicketOrder: Boolean,\n    val isRefund: Boolean,\n    val paymentKey: String?,\n    val itemId: Long,\n    @get:JvmName(\"getIsUsingCoupon\") val isUsingCoupon: Boolean,\n    val issuedCouponId: Long?,\n) : DomainEvent() {\n    companion object {\n        @JvmStatic\n        fun from(order: Order): WithDrawOrderEvent = WithDrawOrderEvent(\n            orderMethod = order.orderMethod!!,\n            paymentKey = if (order.isNeedPaid()) order.paymentKey else null,\n            userId = order.userId!!,\n            orderUuid = order.uuid!!,\n            orderStatus = order.orderStatus!!,\n            itemId = order.itemId,\n            isUsingCoupon = order.hasCoupon(),\n            issuedCouponId = order.orderCouponVo.couponId,\n            isDudoongTicketOrder = order.isDudoongTicketOrder(),\n            isRefund = order.orderStatus == OrderStatus.REFUND,\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/events/user/UserRegisterEvent.kt",
    "content": "package band.gosrock.domain.common.events.user\n\nimport band.gosrock.domain.common.aop.domainEvent.DomainEvent\n\nclass UserRegisterEvent(val userId: Long?) : DomainEvent() {\n    override fun toString(): String = \"UserRegisterEvent(userId=$userId)\"\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/model/BaseTimeEntity.kt",
    "content": "package band.gosrock.domain.common.model\n\nimport java.time.LocalDateTime\nimport jakarta.persistence.Column\nimport jakarta.persistence.EntityListeners\nimport jakarta.persistence.MappedSuperclass\nimport org.springframework.data.annotation.CreatedDate\nimport org.springframework.data.annotation.LastModifiedDate\nimport org.springframework.data.jpa.domain.support.AuditingEntityListener\n\n@MappedSuperclass\n@EntityListeners(AuditingEntityListener::class)\nabstract class BaseTimeEntity {\n\n    @Column(updatable = false)\n    @CreatedDate\n    var createdAt: LocalDateTime? = null\n        protected set\n\n    @Column\n    @LastModifiedDate\n    var updatedAt: LocalDateTime? = null\n        protected set\n\n    /** Kotlin 호환용 메서드 - 같은 모듈에서 Kotlin이 Lombok getter에 접근 불가한 문제 우회 */\n    fun createdAtKt(): LocalDateTime = this.createdAt ?: throw IllegalStateException(\"Entity has not been persisted yet\")\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/util/PhoneNumberInstance.kt",
    "content": "package band.gosrock.domain.common.util\n\nimport com.google.i18n.phonenumbers.PhoneNumberUtil\n\nobject PhoneNumberInstance {\n    @JvmField\n    val instance: PhoneNumberUtil = PhoneNumberUtil.getInstance()\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/util/QueryDslUtil.kt",
    "content": "package band.gosrock.domain.common.util\n\nimport com.querydsl.core.types.Order\nimport com.querydsl.core.types.OrderSpecifier\nimport com.querydsl.core.types.Path\nimport com.querydsl.core.types.dsl.Expressions\nimport com.querydsl.core.types.dsl.PathBuilder\nimport org.springframework.data.domain.Pageable\n\n/** QueryDsl 에서 compileQuerydsl 빌드를 통해 생성된 클래스 객체 타입을 받아 Sort 의 대상이 되는 Q타입 클래스 객체 리스트를 전달 */\nobject QueryDslUtil {\n    @Suppress(\"UNCHECKED_CAST\")\n    @JvmStatic\n    fun <T> getOrderSpecifiers(type: Class<out T>, pageable: Pageable): Array<OrderSpecifier<*>> {\n        val variable = type.simpleName.lowercase()\n        val orderSpecifiers = mutableListOf<OrderSpecifier<*>>()\n        val entityPath = PathBuilder(type, variable)\n        for (order in pageable.sort) {\n            if (hasField(type, order.property)) {\n                val path = entityPath.get(order.property) as PathBuilder<Comparable<Any>>\n                orderSpecifiers.add(OrderSpecifier(Order.valueOf(order.direction.name), path))\n            }\n        }\n        return orderSpecifiers.toTypedArray()\n    }\n\n    private fun <T> hasField(type: Class<out T>, name: String): Boolean =\n        type.declaredFields.any { it.name == name }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    @JvmStatic\n    fun getSortedColumn(order: Order, parent: Path<*>, fieldName: String): OrderSpecifier<*> {\n        val fieldPath = Expressions.path(Any::class.java, parent, fieldName) as com.querydsl.core.types.dsl.SimpleExpression<Comparable<Any>>\n        return OrderSpecifier(order, fieldPath)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/util/SliceUtil.kt",
    "content": "package band.gosrock.domain.common.util\n\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.domain.Slice\nimport org.springframework.data.domain.SliceImpl\n\nobject SliceUtil {\n    @JvmStatic\n    fun <T> valueOf(contents: List<T>, pageable: Pageable): Slice<T> {\n        val hasNext = hasNext(contents, pageable)\n        return SliceImpl(if (hasNext) getContent(contents, pageable) else contents, pageable, hasNext)\n    }\n\n    private fun <T> hasNext(content: List<T>, pageable: Pageable): Boolean =\n        pageable.isPaged && content.size > pageable.pageSize\n\n    private fun <T> getContent(content: List<T>, pageable: Pageable): List<T> =\n        content.subList(0, pageable.pageSize)\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/AccountInfoVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport jakarta.persistence.Embeddable\n\n@Embeddable\nclass AccountInfoVo(\n    var bankName: String? = null,\n    var accountNumber: String? = null,\n    var accountHolder: String? = null,\n) {\n    companion object {\n        @JvmStatic\n        fun valueOf(bankName: String?, accountNumber: String?, accountHolder: String?): AccountInfoVo =\n            AccountInfoVo(bankName, accountNumber, accountHolder)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/CommentInfoVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.common.annotation.DateFormat\nimport band.gosrock.domain.domains.comment.domain.Comment\nimport java.time.LocalDateTime\n\nclass CommentInfoVo private constructor(\n    val commentId: Long?,\n    val nickName: String?,\n    val content: String?,\n    @field:DateFormat val createdAt: LocalDateTime?,\n    val eventId: Long?,\n    val userId: Long?,\n) {\n    companion object {\n        @JvmStatic\n        fun from(comment: Comment): CommentInfoVo =\n            CommentInfoVo(\n                commentId = comment.id,\n                nickName = comment.nickName,\n                content = comment.content,\n                createdAt = comment.createdAtKt(),\n                eventId = comment.eventId,\n                userId = comment.user?.id,\n            )\n\n        @JvmStatic\n        fun builder() = Builder()\n    }\n\n    class Builder {\n        private var commentId: Long? = null\n        private var nickName: String? = null\n        private var content: String? = null\n        private var createdAt: LocalDateTime? = null\n        private var eventId: Long? = null\n        private var userId: Long? = null\n\n        fun commentId(v: Long?) = apply { commentId = v }\n        fun nickName(v: String?) = apply { nickName = v }\n        fun content(v: String?) = apply { content = v }\n        fun createdAt(v: LocalDateTime?) = apply { createdAt = v }\n        fun eventId(v: Long?) = apply { eventId = v }\n        fun userId(v: Long?) = apply { userId = v }\n        fun build() = CommentInfoVo(commentId, nickName, content, createdAt, eventId, userId)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/DateTimePeriod.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport java.time.LocalDateTime\nimport jakarta.persistence.Embeddable\n\n@Embeddable\nopen class DateTimePeriod(\n    // 쿠폰 발행 시작 시각\n    var startAt: LocalDateTime? = null,\n    // 쿠폰 발행 마감 시각\n    var endAt: LocalDateTime? = null,\n) {\n    fun contains(datetime: LocalDateTime): Boolean {\n        val start = startAt ?: return false\n        val end = endAt ?: return false\n        return (datetime.isAfter(start) || datetime == start) &&\n            (datetime.isBefore(end) || datetime == end)\n    }\n\n    companion object {\n        @JvmStatic\n        fun between(startAt: LocalDateTime?, endAt: LocalDateTime?): DateTimePeriod =\n            DateTimePeriod(startAt, endAt)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/EventBasicVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.common.annotation.DateFormat\nimport band.gosrock.domain.domains.event.domain.Event\nimport java.time.LocalDateTime\n\ndata class EventBasicVo(\n    val name: String? = null,\n    @DateFormat val startAt: LocalDateTime? = null,\n    @DateFormat val endAt: LocalDateTime? = null,\n    val runTime: Long? = null,\n) {\n    companion object {\n        @JvmStatic\n        fun from(event: Event): EventBasicVo {\n            val eventBasic = event.eventBasic ?: return EventBasicVo()\n            return EventBasicVo(\n                name = eventBasic.name,\n                startAt = eventBasic.startAt,\n                endAt = event.getEndAt(),\n                runTime = eventBasic.runTime,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/EventDetailVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.domain.domains.event.domain.Event\n\ndata class EventDetailVo(\n    // 포스터 이미지\n    val posterImage: ImageVo? = null,\n    // (마크다운) 공연 상세 내용\n    val content: String? = null,\n) {\n    companion object {\n        @JvmStatic\n        fun from(event: Event): EventDetailVo {\n            val eventDetail = event.eventDetail ?: return EventDetailVo()\n            return EventDetailVo(\n                posterImage = eventDetail.posterImage,\n                content = eventDetail.content,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/EventInfoVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.common.annotation.DateFormat\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.event.domain.EventStatus\nimport com.fasterxml.jackson.annotation.JsonUnwrapped\nimport java.time.LocalDateTime\n\n/*\n이벤트 정보 VO (공용)\n */\ndata class EventInfoVo(\n    /** 이벤트 이름 */\n    val eventName: String? = null,\n    /** 이벤트 디테일 */\n    @JsonUnwrapped val eventDetailVo: EventDetailVo? = null,\n    /** 이벤트 시작 시간 */\n    @DateFormat val startAt: LocalDateTime? = null,\n    /** 이벤트 종료 시간 */\n    @DateFormat val endAt: LocalDateTime? = null,\n    /** 공연 장소 */\n    @JsonUnwrapped val eventPlace: EventPlaceVo? = null,\n    /** 공연 상태 */\n    val eventStatus: EventStatus? = null,\n) {\n    companion object {\n        @JvmStatic\n        fun from(event: Event): EventInfoVo =\n            EventInfoVo(\n                eventName = EventBasicVo.from(event).name,\n                eventDetailVo = EventDetailVo.from(event),\n                startAt = event.getStartAt(),\n                endAt = event.getEndAt(),\n                eventPlace = EventPlaceVo.from(event),\n                eventStatus = event.status,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/EventPlaceVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.domain.domains.event.domain.Event\n\ndata class EventPlaceVo(\n    // (지도 정보) 위도 - x\n    val latitude: Double? = null,\n    // (지도 정보) 경도 - y\n    val longitude: Double? = null,\n    // 공연 장소\n    val placeName: String? = null,\n    // 공연 상세 주소\n    val placeAddress: String? = null,\n) {\n    companion object {\n        @JvmStatic\n        fun from(event: Event): EventPlaceVo {\n            val eventPlace = event.eventPlace ?: return EventPlaceVo()\n            return EventPlaceVo(\n                latitude = eventPlace.latitude,\n                longitude = eventPlace.longitude,\n                placeName = eventPlace.placeName,\n                placeAddress = eventPlace.placeAddress,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/EventProfileVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.common.annotation.DateFormat\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.event.domain.EventStatus\nimport java.time.LocalDateTime\n\ndata class EventProfileVo(\n    val eventId: Long? = null,\n    val posterImage: ImageVo? = null,\n    val name: String? = null,\n    @DateFormat val startAt: LocalDateTime? = null,\n    @DateFormat val endAt: LocalDateTime? = null,\n    val runTime: Long? = null,\n    val placeName: String? = null,\n    val status: EventStatus? = null,\n) {\n    companion object {\n        @JvmStatic\n        fun from(event: Event): EventProfileVo {\n            val eventBasicVo = event.toEventBasicVo()\n            val eventPlaceVo = event.toEventPlaceVo()\n            val eventDetailVo = event.toEventDetailVo()\n            return EventProfileVo(\n                eventId = event.id,\n                posterImage = eventDetailVo.posterImage,\n                name = eventBasicVo.name,\n                startAt = eventBasicVo.startAt,\n                endAt = event.getEndAt(),\n                runTime = eventBasicVo.runTime,\n                placeName = eventPlaceVo.placeName,\n                status = event.status,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/HostInfoVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.domain.domains.host.domain.Host\n\nclass HostInfoVo private constructor(\n    val hostId: Long?,\n    val name: String?,\n    val introduce: String?,\n    val profileImage: ImageVo?,\n    val contactEmail: String?,\n    val contactNumber: String?,\n    val partner: Boolean?,\n) {\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (other !is HostInfoVo) return false\n        return hostId == other.hostId && name == other.name && introduce == other.introduce &&\n            profileImage == other.profileImage && contactEmail == other.contactEmail &&\n            contactNumber == other.contactNumber && partner == other.partner\n    }\n\n    override fun hashCode(): Int = java.util.Objects.hash(hostId, name, introduce, profileImage, contactEmail, contactNumber, partner)\n\n    companion object {\n        @JvmStatic\n        fun from(host: Host): HostInfoVo =\n            HostInfoVo(\n                hostId = host.id,\n                name = host.profile?.name,\n                introduce = host.profile?.introduce,\n                profileImage = host.profile?.profileImage,\n                contactEmail = host.profile?.contactEmail,\n                contactNumber = host.profile?.contactNumber,\n                partner = host.partner,\n            )\n\n        @JvmStatic\n        fun builder() = Builder()\n    }\n\n    class Builder {\n        private var hostId: Long? = null\n        private var name: String? = null\n        private var introduce: String? = null\n        private var profileImage: ImageVo? = null\n        private var contactEmail: String? = null\n        private var contactNumber: String? = null\n        private var partner: Boolean? = null\n\n        fun hostId(v: Long?) = apply { hostId = v }\n        fun name(v: String?) = apply { name = v }\n        fun introduce(v: String?) = apply { introduce = v }\n        fun profileImage(v: ImageVo?) = apply { profileImage = v }\n        fun contactEmail(v: String?) = apply { contactEmail = v }\n        fun contactNumber(v: String?) = apply { contactNumber = v }\n        fun partner(v: Boolean?) = apply { partner = v }\n        fun build() = HostInfoVo(hostId, name, introduce, profileImage, contactEmail, contactNumber, partner)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/HostProfileVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.domain.domains.host.domain.Host\n\nclass HostProfileVo(\n    val hostId: Long?,\n    val name: String?,\n    val introduce: String?,\n    val profileImage: ImageVo?,\n) {\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (other !is HostProfileVo) return false\n        return hostId == other.hostId && name == other.name && introduce == other.introduce && profileImage == other.profileImage\n    }\n\n    override fun hashCode(): Int = java.util.Objects.hash(hostId, name, introduce, profileImage)\n\n    companion object {\n        @JvmStatic\n        fun from(host: Host): HostProfileVo =\n            HostProfileVo(\n                hostId = host.id,\n                name = host.profile?.name,\n                introduce = host.profile?.introduce,\n                profileImage = host.profile?.profileImage,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/HostUserVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.domain.domains.host.domain.HostRole\nimport band.gosrock.domain.domains.host.domain.HostUser\nimport band.gosrock.domain.domains.user.domain.User\nimport com.fasterxml.jackson.annotation.JsonUnwrapped\n\nclass HostUserVo(\n    @JsonUnwrapped val userInfoVo: UserInfoVo,\n    val role: HostRole?,\n    val active: Boolean?,\n) {\n    companion object {\n        @JvmStatic\n        fun from(user: User, hostUser: HostUser): HostUserVo =\n            HostUserVo(\n                userInfoVo = user.toUserInfoVo(),\n                active = hostUser.active,\n                role = hostUser.role,\n            )\n\n        @JvmStatic\n        fun from(userInfoVo: UserInfoVo, hostUser: HostUser): HostUserVo =\n            HostUserVo(\n                userInfoVo = userInfoVo,\n                active = hostUser.active,\n                role = hostUser.role,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/ImageVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.common.consts.DuDoongStatic.assetDomain\nimport com.fasterxml.jackson.annotation.JsonValue\nimport jakarta.persistence.Embeddable\n\n@Embeddable\nclass ImageVo(\n    var imageKey: String? = null,\n) {\n    @JsonValue\n    fun generateImageUrl(): String? {\n        val key = imageKey ?: return null\n        // 카카오 이미지로 회원가입한경우 대응\n        if (key.contains(\"kakao\")) return key\n        // 현재 도메인 대응\n        return assetDomain + key\n    }\n\n    companion object {\n        @JvmStatic\n        fun valueOf(key: String?): ImageVo = ImageVo(key)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/IssuedCouponInfoVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.common.annotation.DateFormat\nimport band.gosrock.domain.domains.coupon.domain.ApplyTarget\nimport band.gosrock.domain.domains.coupon.domain.DiscountType\nimport band.gosrock.domain.domains.coupon.domain.IssuedCoupon\nimport java.time.LocalDateTime\n\ndata class IssuedCouponInfoVo(\n    val issuedCouponId: Long? = null,\n    // 사용여부\n    val usageStatus: Boolean? = null,\n    val applyTarget: ApplyTarget? = null,\n    val couponCode: String? = null,\n    // 정률할인, 정액할인\n    val discountType: DiscountType? = null,\n    val discountAmount: Long? = null,\n    // 쿠폰 사용 가능 마감 시각\n    @DateFormat val validDateTime: LocalDateTime? = null,\n    val minimumCost: Long? = null,\n) {\n    companion object {\n        @JvmStatic\n        fun of(issuedCoupon: IssuedCoupon): IssuedCouponInfoVo =\n            IssuedCouponInfoVo(\n                issuedCouponId = issuedCoupon.id,\n                usageStatus = issuedCoupon.usageStatus,\n                applyTarget = issuedCoupon.couponCampaign?.applyTarget,\n                couponCode = issuedCoupon.couponCampaign?.couponCode,\n                discountType = issuedCoupon.couponCampaign?.discountType,\n                discountAmount = issuedCoupon.couponCampaign?.discountAmount,\n                validDateTime = issuedCoupon.calculateValidTerm(),\n                minimumCost = issuedCoupon.couponCampaign?.minimumCost,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/IssuedTicketInfoVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.common.annotation.DateFormat\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketStatus\nimport band.gosrock.domain.domains.ticket_item.domain.TicketPayType\nimport java.time.LocalDateTime\n\ndata class IssuedTicketInfoVo(\n    /** 발급 티켓 id */\n    val issuedTicketId: Long? = null,\n    /** 발급 티켓 번호 Ex. T10000001 */\n    val issuedTicketNo: String? = null,\n    /** 발급 티켓 uuid */\n    val uuid: String? = null,\n    /** 발급 티켓 종류 */\n    val ticketName: String? = null,\n    /** 발급 티켓 지불 방식 */\n    val payType: TicketPayType? = null,\n    /** 발급 티켓 가격 */\n    val ticketPrice: Money? = null,\n    /** 티켓 발급 시간 */\n    @DateFormat val createdAt: LocalDateTime? = null,\n    @DateFormat val enteredAt: LocalDateTime? = null,\n    /** 발급 티켓 상태 */\n    val issuedTicketStatus: IssuedTicketStatus? = null,\n    /** 발급 티켓 옵션 금액 합계 */\n    val optionPrice: Money? = null,\n) {\n    companion object {\n        @JvmStatic\n        fun from(issuedTicket: IssuedTicket): IssuedTicketInfoVo =\n            IssuedTicketInfoVo(\n                issuedTicketId = issuedTicket.id,\n                issuedTicketNo = issuedTicket.issuedTicketNo,\n                uuid = issuedTicket.uuid,\n                ticketName = issuedTicket.itemInfo?.ticketName,\n                payType = issuedTicket.itemInfo?.payType,\n                ticketPrice = issuedTicket.itemInfo?.price,\n                createdAt = issuedTicket.createdAt,\n                issuedTicketStatus = issuedTicket.issuedTicketStatus,\n                optionPrice = issuedTicket.sumOptionPrice(),\n                enteredAt = issuedTicket.enteredAt,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/IssuedTicketOptionAnswerVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketOptionAnswer\n\ndata class IssuedTicketOptionAnswerVo(\n    val issuedTicketOptionAnswerId: Long? = null,\n    val optionQuestion: String? = null,\n    val answer: String? = null,\n    val additionalPrice: Money? = null,\n) {\n    companion object {\n        @JvmStatic\n        fun from(issuedTicketOptionAnswer: IssuedTicketOptionAnswer): IssuedTicketOptionAnswerVo =\n            IssuedTicketOptionAnswerVo(\n                issuedTicketOptionAnswerId = issuedTicketOptionAnswer.id,\n                answer = issuedTicketOptionAnswer.answer,\n                additionalPrice = issuedTicketOptionAnswer.additionalPrice,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/Money.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.domain.common.converter.BigDecimalScale6WithBankersRoundingConverter\nimport com.fasterxml.jackson.annotation.JsonValue\nimport java.math.BigDecimal\nimport jakarta.persistence.Column\nimport jakarta.persistence.Convert\nimport jakarta.persistence.Embeddable\n\n@Embeddable\nclass Money(\n    // DECIMAL(21,6) 타입에 대한 맵핑 상세 정의\n    @Column(name = \"amount\", nullable = false, precision = 21, scale = 6)\n    @Convert(converter = BigDecimalScale6WithBankersRoundingConverter::class)\n    var amount: BigDecimal = BigDecimal.ZERO,\n) {\n    companion object {\n        @JvmField\n        val ZERO: Money = Money.wons(0)\n\n        @JvmStatic\n        fun wons(amount: Long): Money = Money(BigDecimal.valueOf(amount))\n\n        @JvmStatic\n        fun wons(amount: Double): Money = Money(BigDecimal.valueOf(amount))\n\n        @JvmStatic\n        fun <T> sum(bags: Collection<T>, monetary: (T) -> Money): Money =\n            bags.fold(ZERO) { acc, item -> acc.plus(monetary(item)) }\n    }\n\n    fun plus(amount: Money): Money = Money(this.amount.add(amount.amount))\n    fun minus(amount: Money): Money = Money(this.amount.subtract(amount.amount))\n    fun times(percent: Double): Money = Money(this.amount.multiply(BigDecimal.valueOf(percent)))\n    fun divide(divisor: Double): Money = Money(amount.divide(BigDecimal.valueOf(divisor)))\n\n    fun isLessThan(other: Money): Boolean = amount.compareTo(other.amount) < 0\n    fun isLessThanOrEqual(other: Money): Boolean = amount.compareTo(other.amount) <= 0\n    fun isGreaterThanOrEqual(other: Money): Boolean = amount.compareTo(other.amount) >= 0\n    fun isGreaterThan(other: Money): Boolean = amount.compareTo(other.amount) > 0\n\n    fun longValue(): Long = amount.toLong()\n    fun doubleValue(): Double = amount.toDouble()\n\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (other !is Money) return false\n        return amount.compareTo(other.amount) == 0\n    }\n\n    override fun hashCode(): Int = amount.stripTrailingZeros().hashCode()\n\n    @JsonValue\n    override fun toString(): String = \"${amount.toLong()}원\"\n\n    fun getDiscountAmountByPercentage(supply: Money, percentage: Long): Long {\n        val discountPercent = percentage * 0.01\n        return Math.round(supply.times(discountPercent).longValue().toDouble())\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/OptionAnswerVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.domain.domains.ticket_item.domain.OptionGroupType\n\ndata class OptionAnswerVo(\n    val optionGroupType: OptionGroupType?,\n    val questionName: String?,\n    val questionDescription: String?,\n    val answer: String?,\n    val additionalPrice: Money?,\n)\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/PhoneNumberVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.domain.common.util.PhoneNumberInstance\nimport band.gosrock.domain.domains.user.exception.UserPhoneNumberInvalidException\nimport com.fasterxml.jackson.annotation.JsonValue\nimport com.google.i18n.phonenumbers.NumberParseException\nimport com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat\nimport com.google.i18n.phonenumbers.Phonenumber.PhoneNumber\nimport jakarta.persistence.Embeddable\n\n@Embeddable\nclass PhoneNumberVo(\n    // +82 10-xxxx-xxxx format 으로 저장.\n    var phoneNumber: String? = null,\n) {\n    companion object {\n        @JvmStatic\n        fun valueOf(rawPhoneNumber: String?): PhoneNumberVo = PhoneNumberVo(rawPhoneNumber)\n\n        private fun getPhoneNumber(rawPhoneNumber: String): PhoneNumber {\n            return try {\n                PhoneNumberInstance.instance.parse(rawPhoneNumber, \"KR\")\n            } catch (e: NumberParseException) {\n                throw UserPhoneNumberInvalidException.EXCEPTION\n            }\n        }\n    }\n\n    /** 010-xxxx-xxxx format */\n    @JsonValue\n    fun getNationalFormat(): String {\n        val num = getPhoneNumber(phoneNumber ?: return \"\")\n        return PhoneNumberInstance.instance.format(num, PhoneNumberFormat.NATIONAL)\n    }\n\n    /** +82 10-xxxx-xxxx format */\n    @Throws(NumberParseException::class)\n    fun getInternationalFormat(): String {\n        val num = getPhoneNumber(phoneNumber ?: return \"\")\n        return PhoneNumberInstance.instance.format(num, PhoneNumberFormat.INTERNATIONAL)\n    }\n\n    /** 01000000000 format */\n    @Throws(NumberParseException::class)\n    fun getNaverSmsToNumber(): String = getNationalFormat().replace(\"-\", \"\")\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/RefundInfoVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.common.annotation.DateFormat\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport java.time.LocalDateTime\n\n/** 상품 취소 가능 여부를 반환합니다. */\ndata class RefundInfoVo(\n    @DateFormat val startAt: LocalDateTime,\n    val availAble: Boolean\n) {\n    companion object {\n        @JvmStatic\n        fun from(startAt: LocalDateTime): RefundInfoVo {\n            val before = nowIsBefore(startAt)\n            return RefundInfoVo(startAt = startAt, availAble = before)\n        }\n\n        @JvmStatic\n        fun of(startAt: LocalDateTime, orderStatus: OrderStatus): RefundInfoVo {\n            val availAble = if (nowIsBefore(startAt)) orderStatus.isCanWithDraw() else false\n            return RefundInfoVo(startAt = startAt, availAble = availAble)\n        }\n\n        private fun nowIsBefore(startAt: LocalDateTime): Boolean =\n            LocalDateTime.now().isBefore(startAt)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/UserInfoVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.domain.domains.user.domain.User\nimport java.time.LocalDateTime\n\nclass UserInfoVo(\n    val userId: Long?,\n    val userName: String?,\n    val email: String?,\n    val phoneNumber: PhoneNumberVo?,\n    val profileImage: ImageVo?,\n    val createdAt: LocalDateTime?,\n    var receiveMail: Boolean?,\n    var marketingAgree: Boolean?,\n) {\n    companion object {\n        @JvmStatic\n        fun from(user: User): UserInfoVo =\n            UserInfoVo(\n                userId = user.id,\n                userName = user.profile?.name,\n                email = user.profile?.email,\n                profileImage = user.profile?.profileImage,\n                phoneNumber = user.profile?.phoneNumberVo,\n                createdAt = user.createdAtKt(),\n                receiveMail = user.isReceiveEmail(),\n                marketingAgree = user.isAgreeMarketing(),\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/common/vo/UserProfileVo.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport band.gosrock.domain.domains.user.domain.User\n\nclass UserProfileVo(\n    val userId: Long?,\n    val userName: String?,\n    val email: String?,\n    val profileImage: ImageVo?,\n) {\n    companion object {\n        @JvmStatic\n        fun from(user: User): UserProfileVo =\n            UserProfileVo(\n                userId = user.id,\n                userName = user.profile?.name,\n                email = user.profile?.email,\n                profileImage = user.profile?.profileImage,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/config/CustomAsyncExceptionHandler.kt",
    "content": "package band.gosrock.domain.config\n\nimport band.gosrock.infrastructure.config.slack.SlackAsyncErrorSender\nimport org.slf4j.LoggerFactory\nimport org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler\nimport org.springframework.stereotype.Component\nimport java.lang.reflect.Method\n\n@Component\nclass CustomAsyncExceptionHandler(\n    private val slackAsyncErrorSender: SlackAsyncErrorSender\n) : AsyncUncaughtExceptionHandler {\n\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    override fun handleUncaughtException(throwable: Throwable, method: Method, vararg params: Any?) {\n        log.error(\"Exception message - $throwable\")\n        log.error(\"Method name - ${method.name}\")\n        for (param in params) {\n            log.error(\"Parameter value - $param\")\n        }\n        @Suppress(\"UNCHECKED_CAST\")\n        slackAsyncErrorSender.execute(method.name, throwable, params as Array<Any>)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/config/EnableAsyncConfig.kt",
    "content": "package band.gosrock.domain.config\n\nimport org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler\nimport org.springframework.context.annotation.Configuration\nimport org.springframework.scheduling.annotation.AsyncConfigurer\nimport org.springframework.scheduling.annotation.EnableAsync\nimport org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor\nimport java.util.concurrent.Executor\n\n@EnableAsync\n@Configuration\nclass EnableAsyncConfig(\n    private val customAsyncExceptionHandler: CustomAsyncExceptionHandler\n) : AsyncConfigurer {\n\n    override fun getAsyncExecutor(): Executor {\n        val executor = ThreadPoolTaskExecutor()\n        executor.corePoolSize = 5\n        executor.maxPoolSize = 10\n        executor.queueCapacity = 50\n        executor.setThreadNamePrefix(\"async-\")\n        executor.setTaskDecorator(MdcTaskDecorator())\n        executor.initialize()\n        return executor\n    }\n\n    override fun getAsyncUncaughtExceptionHandler(): AsyncUncaughtExceptionHandler =\n        customAsyncExceptionHandler\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/config/JpaConfig.kt",
    "content": "package band.gosrock.domain.config\n\nimport band.gosrock.domain.DomainPackageLocation\nimport org.springframework.boot.autoconfigure.domain.EntityScan\nimport org.springframework.context.annotation.Configuration\nimport org.springframework.data.jpa.repository.config.EnableJpaAuditing\nimport org.springframework.data.jpa.repository.config.EnableJpaRepositories\n\n@Configuration\n@EnableJpaAuditing\n@EntityScan(basePackageClasses = [DomainPackageLocation::class])\n@EnableJpaRepositories(basePackageClasses = [DomainPackageLocation::class])\nclass JpaConfig\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/config/MdcTaskDecorator.kt",
    "content": "package band.gosrock.domain.config\n\nimport org.slf4j.MDC\nimport org.springframework.core.task.TaskDecorator\n\nclass MdcTaskDecorator : TaskDecorator {\n    override fun decorate(runnable: Runnable): Runnable {\n        val contextMap = MDC.getCopyOfContextMap()\n        return Runnable {\n            if (contextMap != null) {\n                MDC.setContextMap(contextMap)\n            }\n            try {\n                runnable.run()\n            } finally {\n                MDC.clear()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/config/QueryDslConfig.kt",
    "content": "package band.gosrock.domain.config\n\nimport com.querydsl.jpa.impl.JPAQueryFactory\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\nimport jakarta.persistence.EntityManager\nimport jakarta.persistence.PersistenceContext\n\n@Configuration\nclass QueryDslConfig {\n\n    @PersistenceContext\n    private lateinit var entityManager: EntityManager\n\n    @Bean\n    fun jpaQueryFactory(): JPAQueryFactory = JPAQueryFactory(entityManager)\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/cart/adaptor/CartAdaptor.kt",
    "content": "package band.gosrock.domain.domains.cart.adaptor\n\nimport band.gosrock.common.annotation.Adaptor\nimport band.gosrock.domain.domains.cart.domain.Cart\nimport band.gosrock.domain.domains.cart.exception.CartNotFoundException\nimport band.gosrock.domain.domains.cart.repository.CartRepository\nimport java.util.Optional\n\n@Adaptor\nclass CartAdaptor(private val cartRepository: CartRepository) {\n\n    fun save(cart: Cart): Cart = cartRepository.save(cart)\n\n    fun queryCart(cartId: Long): Cart =\n        cartRepository.findById(cartId).orElseThrow { CartNotFoundException.EXCEPTION }\n\n    fun queryCart(cartId: Long, userId: Long): Cart =\n        cartRepository.findByIdAndUserId(cartId, userId)\n            .orElseThrow { CartNotFoundException.EXCEPTION }\n\n    fun findCartByUserId(userId: Long): Optional<Cart> =\n        cartRepository.findByUserId(userId)\n\n    fun find(cartId: Long): Cart =\n        cartRepository.find(cartId).orElseThrow { CartNotFoundException.EXCEPTION }\n\n    fun deleteByUserId(userId: Long) {\n        cartRepository.deleteByUserId(userId)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/cart/domain/Cart.kt",
    "content": "package band.gosrock.domain.domains.cart.domain\n\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.cart.exception.CartLineItemNotFoundException\nimport jakarta.persistence.CascadeType\nimport jakarta.persistence.Column\nimport jakarta.persistence.Entity\nimport jakarta.persistence.FetchType\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\nimport jakarta.persistence.JoinColumn\nimport jakarta.persistence.OneToMany\n\n@Entity(name = \"tbl_cart\")\nclass Cart() : BaseTimeEntity() {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"cart_id\")\n    var id: Long? = null\n        protected set\n\n    @Column(nullable = false)\n    var userId: Long? = null\n        protected set\n\n    var cartName: String? = null\n        protected set\n\n    @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY)\n    @JoinColumn(name = \"cart_id\")\n    var cartLineItems: MutableList<CartLineItem> = mutableListOf()\n        protected set\n\n    companion object {\n        @JvmStatic\n        fun of(\n            cartLineItems: List<CartLineItem>,\n            itemName: String,\n            userId: Long,\n            cartValidator: CartValidator,\n        ): Cart = Cart().apply {\n            this.userId = userId\n            this.cartLineItems.addAll(cartLineItems)\n            cartValidator.validCanCreate(this)\n            updateCartName(itemName)\n        }\n\n        /** 테스트 전용 팩토리 메서드 */\n        @JvmStatic\n        fun forTest(\n            userId: Long? = null,\n            cartLineItems: List<CartLineItem> = emptyList(),\n        ): Cart = Cart().apply {\n            this.userId = userId\n            this.cartLineItems.addAll(cartLineItems)\n        }\n    }\n\n    fun updateCartName(name: String) {\n        cartName = name\n    }\n\n    fun isNeedPaid(): Boolean =\n        cartLineItems.any { it.isNeedPaid() }\n\n    fun getTotalQuantity(): Long =\n        cartLineItems.sumOf { it.quantity!! }\n\n    fun getTotalPrice(): Money =\n        cartLineItems.fold(Money.ZERO) { acc, item -> acc.plus(item.getTotalCartLinePrice()) }\n\n    fun getItemId(): Long = getCartLineItem().itemId!!\n\n    fun getCartLineItem(): CartLineItem =\n        cartLineItems.firstOrNull() ?: throw CartLineItemNotFoundException.EXCEPTION\n\n    fun getDistinctItemIds(): List<Long> =\n        cartLineItems.map { it.itemId!! }.distinct()\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/cart/domain/CartLineItem.kt",
    "content": "package band.gosrock.domain.domains.cart.domain\n\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport jakarta.persistence.CascadeType\nimport jakarta.persistence.Column\nimport jakarta.persistence.Entity\nimport jakarta.persistence.FetchType\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\nimport jakarta.persistence.JoinColumn\nimport jakarta.persistence.OneToMany\n\n@Entity(name = \"tbl_cart_line_item\")\nclass CartLineItem() : BaseTimeEntity() {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"cart_line_id\")\n    var id: Long? = null\n        protected set\n\n    @Column(nullable = false)\n    var itemId: Long? = null\n        protected set\n\n    @Column(nullable = false)\n    var itemPrice: Money? = null\n        protected set\n\n    @Column(nullable = false)\n    var quantity: Long? = null\n        protected set\n\n    @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY)\n    @JoinColumn(name = \"cart_line_id\")\n    var cartOptionAnswers: MutableList<CartOptionAnswer> = mutableListOf()\n        protected set\n\n    companion object {\n        @JvmStatic\n        fun of(item: TicketItem, quantity: Long, cartOptionAnswers: List<CartOptionAnswer>): CartLineItem =\n            CartLineItem().apply {\n                this.itemId = item.id\n                this.itemPrice = item.price\n                this.quantity = quantity\n                this.cartOptionAnswers.addAll(cartOptionAnswers)\n            }\n    }\n\n    fun getTotalOptionsPrice(): Money =\n        cartOptionAnswers.fold(Money.ZERO) { acc, answer -> acc.plus(answer.additionalPrice) }\n\n    fun getTotalCartLinePrice(): Money {\n        val price = itemPrice ?: throw IllegalStateException(\"CartLineItem itemPrice is not initialized\")\n        val qty = quantity ?: throw IllegalStateException(\"CartLineItem quantity is not initialized\")\n        return price.plus(getTotalOptionsPrice()).times(qty.toDouble())\n    }\n\n    fun isNeedPaid(): Boolean = Money.ZERO.isLessThan(getTotalCartLinePrice())\n\n    fun getAnswerOptionIds(): List<Long> =\n        cartOptionAnswers.map { it.optionId!! }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/cart/domain/CartOptionAnswer.kt",
    "content": "package band.gosrock.domain.domains.cart.domain\n\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.common.vo.OptionAnswerVo\nimport band.gosrock.domain.domains.ticket_item.domain.Option\nimport jakarta.persistence.Column\nimport jakarta.persistence.Entity\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\n\n@Entity(name = \"tbl_cart_option_answer\")\nclass CartOptionAnswer() : BaseTimeEntity() {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"cart_option_answer_id\")\n    var id: Long? = null\n        protected set\n\n    @Column(nullable = false)\n    var optionId: Long? = null\n        protected set\n\n    @Column(nullable = false)\n    var additionalPrice: Money = Money.ZERO\n        protected set\n\n    var answer: String? = null\n        protected set\n\n    companion object {\n        @JvmStatic\n        fun of(option: Option, answer: String): CartOptionAnswer = CartOptionAnswer().apply {\n            this.optionId = option.id\n            this.additionalPrice = option.additionalPrice ?: Money.ZERO\n            this.answer = answer\n        }\n    }\n\n    fun getOptionAnswerVo(option: Option): OptionAnswerVo =\n        OptionAnswerVo(\n            questionDescription = option.getQuestionDescription(),\n            optionGroupType = option.getQuestionType(),\n            questionName = option.getQuestionName(),\n            answer = answer,\n            additionalPrice = option.additionalPrice,\n        )\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/cart/domain/CartValidator.kt",
    "content": "package band.gosrock.domain.domains.cart.domain\n\nimport band.gosrock.common.annotation.Validator\nimport band.gosrock.domain.domains.cart.exception.CartItemNotOneTypeException\nimport band.gosrock.domain.domains.cart.exception.CartNotAnswerAllOptionGroupException\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.issuedTicket.adaptor.IssuedTicketAdaptor\nimport band.gosrock.domain.domains.ticket_item.adaptor.OptionAdaptor\nimport band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor\nimport band.gosrock.domain.domains.ticket_item.domain.Option\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport java.util.Objects\n\n@Validator\nclass CartValidator(\n    private val itemAdaptor: TicketItemAdaptor,\n    private val issuedTicketAdaptor: IssuedTicketAdaptor,\n    private val eventAdaptor: EventAdaptor,\n    private val optionAdaptor: OptionAdaptor,\n) {\n    fun validCanCreate(cart: Cart) {\n        validItemKindIsOneType(cart)\n        validCorrectAnswer(cart)\n        val item = getItem(cart)\n        val event = eventAdaptor.findById(item.eventId!!)\n        validAnswerToAllQuestion(cart, item)\n        validEventIsOpen(event)\n        validTicketingTime(event)\n        validItemStockEnough(cart, item)\n        validItemPurchaseLimit(cart, item)\n    }\n\n    fun validItemPurchaseLimit(cart: Cart, item: TicketItem) {\n        val paidTicketCount = issuedTicketAdaptor.countPaidTicket(cart.userId!!, item.id!!)\n        val totalIssuedCount = paidTicketCount + cart.getTotalQuantity()\n        item.validPurchaseLimit(totalIssuedCount)\n    }\n\n    fun validTicketingTime(event: Event) {\n        event.validateTicketingTime()\n    }\n\n    fun validItemStockEnough(cart: Cart, item: TicketItem) {\n        item.validEnoughQuantity(cart.getTotalQuantity())\n    }\n\n    fun validEventIsOpen(event: Event) {\n        event.validateNotOpenStatus()\n    }\n\n    fun validItemKindIsOneType(cart: Cart) {\n        val itemIds = cart.getDistinctItemIds()\n        if (itemIds.size != 1) {\n            throw CartItemNotOneTypeException.EXCEPTION\n        }\n    }\n\n    fun validAnswerToAllQuestion(cart: Cart, item: TicketItem) {\n        val cartLineItems = cart.cartLineItems\n        val itemsOptionGroupIds = item.getOptionGroupIds()\n        cartLineItems.forEach { cartLineItem ->\n            if (!Objects.equals(getAnswerOptionGroupIds(cartLineItem), itemsOptionGroupIds)) {\n                throw CartNotAnswerAllOptionGroupException.EXCEPTION\n            }\n        }\n    }\n\n    fun validCorrectAnswer(cart: Cart) {\n        val cartLineItems = cart.cartLineItems\n        cartLineItems.forEach { cartLineItem ->\n            val cartOptionAnswers = cartLineItem.cartOptionAnswers\n            val options = getOptionsFrom(cartLineItem)\n            cartOptionAnswers.forEach { cartOptionAnswer ->\n                val optionId = cartOptionAnswer.optionId!!\n                findOptionFromCartOptionAnswer(options, optionId)\n                    .validCorrectAnswer(cartOptionAnswer.answer ?: \"\")\n            }\n        }\n    }\n\n    private fun getOptionsFrom(cartLineItem: CartLineItem): List<Option> =\n        optionAdaptor.findAllByIds(cartLineItem.getAnswerOptionIds())\n\n    private fun findOptionFromCartOptionAnswer(options: List<Option>, optionId: Long): Option =\n        options.first { it.id == optionId }\n\n    private fun getAnswerOptionGroupIds(cartLineItem: CartLineItem): List<Long> {\n        val answerOptions = getOptionsFrom(cartLineItem)\n        return answerOptions.map { it.getOptionGroupId()!! }.sorted()\n    }\n\n    private fun getItem(cart: Cart): TicketItem =\n        itemAdaptor.queryTicketItem(cart.getItemId())\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/cart/exception/CartErrorCode.kt",
    "content": "package band.gosrock.domain.domains.cart.exception\n\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.consts.DuDoongStatic.BAD_REQUEST\nimport band.gosrock.common.consts.DuDoongStatic.NOT_FOUND\nimport band.gosrock.common.dto.ErrorReason\nimport band.gosrock.common.exception.BaseErrorCode\nimport java.lang.reflect.Field\n\nenum class CartErrorCode(\n    private val status: Int,\n    private val code: String,\n    private val reason: String,\n) : BaseErrorCode {\n    @ExplainError(\"id로 카트를 찾을 때 못 찾으면 발생하는 오류\")\n    CART_NOT_FOUND(NOT_FOUND, \"Cart_404_1\", \"장바구니를 찾을 수 없습니다.\"),\n\n    @ExplainError(\"한 장바구니엔 관련된 한 아이템만 올수 있음\")\n    CART_INVALID_ITEM_KIND_POLICY(BAD_REQUEST, \"Cart_400_1\", \"장바구니에 아이템을 담는 정책을 위반하였습니다.\"),\n    CART_INVALID_OPTION_ANSWER(BAD_REQUEST, \"Cart_400_2\", \"옵션을 잘못 응답 하였습니다.\"),\n    CART_LINE_NOT_FOUND(BAD_REQUEST, \"Cart_400_3\", \"장바구니 안에 상품을 찾을 수 없습니다.\"),\n    CART_NOT_ALL_ANSWER(BAD_REQUEST, \"Cart_400_4\", \"모든 질문에 답변을 하지 않았습니다.\");\n\n    override fun getErrorReason(): ErrorReason =\n        ErrorReason(status = status, code = code, reason = reason)\n\n    override fun getExplainError(): String {\n        val field: Field = this.javaClass.getField(this.name)\n        val annotation: ExplainError? = field.getAnnotation(ExplainError::class.java)\n        return annotation?.value ?: this.reason\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/cart/exception/CartInvalidOptionAnswerException.kt",
    "content": "package band.gosrock.domain.domains.cart.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CartInvalidOptionAnswerException private constructor() : DuDoongCodeException(CartErrorCode.CART_INVALID_OPTION_ANSWER) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CartInvalidOptionAnswerException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/cart/exception/CartItemNotOneTypeException.kt",
    "content": "package band.gosrock.domain.domains.cart.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CartItemNotOneTypeException private constructor() : DuDoongCodeException(CartErrorCode.CART_INVALID_ITEM_KIND_POLICY) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CartItemNotOneTypeException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/cart/exception/CartLineItemNotFoundException.kt",
    "content": "package band.gosrock.domain.domains.cart.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CartLineItemNotFoundException private constructor() : DuDoongCodeException(CartErrorCode.CART_LINE_NOT_FOUND) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CartLineItemNotFoundException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/cart/exception/CartNotAnswerAllOptionGroupException.kt",
    "content": "package band.gosrock.domain.domains.cart.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CartNotAnswerAllOptionGroupException private constructor() : DuDoongCodeException(CartErrorCode.CART_NOT_ALL_ANSWER) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CartNotAnswerAllOptionGroupException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/cart/exception/CartNotFoundException.kt",
    "content": "package band.gosrock.domain.domains.cart.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CartNotFoundException private constructor() : DuDoongCodeException(CartErrorCode.CART_NOT_FOUND) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CartNotFoundException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/cart/repository/CartCustomRepository.kt",
    "content": "package band.gosrock.domain.domains.cart.repository\n\nimport band.gosrock.domain.domains.cart.domain.Cart\nimport java.util.Optional\n\ninterface CartCustomRepository {\n    fun find(cartId: Long): Optional<Cart>\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/cart/repository/CartCustomRepositoryImpl.kt",
    "content": "package band.gosrock.domain.domains.cart.repository\n\nimport band.gosrock.domain.domains.cart.domain.Cart\nimport band.gosrock.domain.domains.cart.domain.QCart.cart\nimport band.gosrock.domain.domains.cart.domain.QCartLineItem.cartLineItem\nimport com.querydsl.jpa.impl.JPAQueryFactory\nimport java.util.Optional\n\nclass CartCustomRepositoryImpl(\n    private val queryFactory: JPAQueryFactory\n) : CartCustomRepository {\n\n    override fun find(cartId: Long): Optional<Cart> {\n        val findCart = queryFactory\n            .selectFrom(cart)\n            .leftJoin(cart.cartLineItems, cartLineItem)\n            .fetchJoin()\n            .where(cart.id.eq(cartId))\n            .fetchOne()\n        return Optional.ofNullable(findCart)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/cart/repository/CartRepository.kt",
    "content": "package band.gosrock.domain.domains.cart.repository\n\nimport band.gosrock.domain.domains.cart.domain.Cart\nimport org.springframework.data.jpa.repository.JpaRepository\nimport java.util.Optional\n\ninterface CartRepository : JpaRepository<Cart, Long>, CartCustomRepository {\n    fun findByIdAndUserId(id: Long, userId: Long): Optional<Cart>\n    fun findByUserId(userId: Long): Optional<Cart>\n    fun deleteByUserId(userId: Long): Optional<Cart>\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/cart/service/CartDomainService.kt",
    "content": "package band.gosrock.domain.domains.cart.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.common.aop.redissonLock.RedissonLock\nimport band.gosrock.domain.domains.cart.adaptor.CartAdaptor\nimport band.gosrock.domain.domains.cart.domain.Cart\n\n@DomainService\nclass CartDomainService(private val cartAdaptor: CartAdaptor) {\n\n    @RedissonLock(LockName = \"카트생성\", paramClassType = Cart::class, identifier = \"userId\", needSameTransaction = true)\n    fun createCart(cart: Cart, userId: Long): Long {\n        cartAdaptor.deleteByUserId(userId)\n        val savedCart = cartAdaptor.save(cart)\n        return savedCart.id!!\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/cart/service/DoneOrderEventHandler.kt",
    "content": "package band.gosrock.domain.domains.cart.service\n\nimport band.gosrock.domain.common.events.order.DoneOrderEvent\nimport band.gosrock.domain.domains.cart.adaptor.CartAdaptor\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.annotation.Propagation\nimport org.springframework.transaction.annotation.Transactional\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\n@Component\nclass DoneOrderEventHandler(private val cartAdaptor: CartAdaptor) {\n\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    @Async\n    @Transactional(propagation = Propagation.REQUIRES_NEW)\n    @TransactionalEventListener(classes = [DoneOrderEvent::class], phase = TransactionPhase.AFTER_COMMIT)\n    fun handleDoneOrderEvent(doneOrderEvent: DoneOrderEvent) {\n        log.info(\"${doneOrderEvent.orderUuid} 주문 상태 완료, 장바구니를 제거합니다.\")\n        val userId = doneOrderEvent.userId\n        cartAdaptor.deleteByUserId(userId)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/comment/adaptor/CommentAdaptor.kt",
    "content": "package band.gosrock.domain.domains.comment.adaptor\n\nimport band.gosrock.common.annotation.Adaptor\nimport band.gosrock.domain.domains.comment.domain.Comment\nimport band.gosrock.domain.domains.comment.dto.condition.CommentCondition\nimport band.gosrock.domain.domains.comment.exception.CommentNotFoundException\nimport band.gosrock.domain.domains.comment.repository.CommentRepository\nimport org.springframework.data.domain.Slice\n\n@Adaptor\nclass CommentAdaptor(\n    private val commentRepository: CommentRepository,\n) {\n    fun save(comment: Comment): Comment = commentRepository.save(comment)\n\n    fun searchComment(commentCondition: CommentCondition): Slice<Comment> =\n        commentRepository.searchToPage(commentCondition)\n\n    fun queryComment(commentId: Long): Comment =\n        commentRepository.findById(commentId).orElseThrow { CommentNotFoundException.EXCEPTION }\n\n    fun queryCommentCount(eventId: Long): Long = commentRepository.countComment(eventId)\n\n    fun queryRandomComment(eventId: Long, limit: Long): List<Comment> {\n        queryCommentCount(eventId)\n        return commentRepository.findAllRandom(eventId, limit)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/comment/domain/Comment.kt",
    "content": "package band.gosrock.domain.domains.comment.domain\n\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport band.gosrock.domain.common.vo.CommentInfoVo\nimport band.gosrock.domain.domains.comment.exception.CommentAlreadyDeleteException\nimport band.gosrock.domain.domains.comment.exception.CommentNotMatchEventException\nimport band.gosrock.domain.domains.user.domain.User\nimport jakarta.persistence.Column\nimport jakarta.persistence.Entity\nimport jakarta.persistence.EnumType\nimport jakarta.persistence.Enumerated\nimport jakarta.persistence.FetchType\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\nimport jakarta.persistence.JoinColumn\nimport jakarta.persistence.ManyToOne\n\n@Entity(name = \"tbl_comment\")\nclass Comment() : BaseTimeEntity() {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"comment_id\")\n    var id: Long? = null\n        protected set\n\n    @Column(length = 200)\n    var content: String? = null\n        protected set\n\n    @Column(length = 15)\n    var nickName: String? = null\n        protected set\n\n    @ManyToOne(fetch = FetchType.LAZY)\n    @JoinColumn(name = \"userId\")\n    var user: User? = null\n        protected set\n\n    var eventId: Long? = null\n        protected set\n\n    @Enumerated(EnumType.STRING)\n    var commentStatus: CommentStatus? = null\n        protected set\n\n    constructor(\n        content: String?,\n        nickName: String?,\n        user: User?,\n        eventId: Long?,\n        commentStatus: CommentStatus?,\n    ) : this() {\n        this.content = content\n        this.nickName = nickName\n        this.user = user\n        this.eventId = eventId\n        this.commentStatus = commentStatus\n    }\n\n    fun toCommentInfoVo(): CommentInfoVo = CommentInfoVo.from(this)\n\n    fun delete() {\n        if (this.commentStatus == CommentStatus.INACTIVE) {\n            throw CommentAlreadyDeleteException.EXCEPTION\n        }\n        this.commentStatus = CommentStatus.INACTIVE\n    }\n\n    fun checkEvent(eventId: Long?) {\n        if (eventId != this.eventId) {\n            throw CommentNotMatchEventException.EXCEPTION\n        }\n    }\n\n    companion object {\n        @JvmStatic\n        fun create(content: String?, nickName: String?, user: User?, eventId: Long?): Comment =\n            Comment(content, nickName, user, eventId, CommentStatus.ACTIVE)\n\n        @JvmStatic\n        fun builder() = Builder()\n    }\n\n    class Builder {\n        private var content: String? = null\n        private var nickName: String? = null\n        private var user: User? = null\n        private var eventId: Long? = null\n        private var commentStatus: CommentStatus? = null\n\n        fun content(v: String?) = apply { content = v }\n        fun nickName(v: String?) = apply { nickName = v }\n        fun user(v: User?) = apply { user = v }\n        fun eventId(v: Long?) = apply { eventId = v }\n        fun commentStatus(v: CommentStatus?) = apply { commentStatus = v }\n        fun build() = Comment(content, nickName, user, eventId, commentStatus)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/comment/domain/CommentStatus.kt",
    "content": "package band.gosrock.domain.domains.comment.domain\n\nenum class CommentStatus(val value: String) {\n    ACTIVE(\"ACTIVE\"),\n    INACTIVE(\"INACTIVE\"),\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/comment/dto/condition/CommentCondition.kt",
    "content": "package band.gosrock.domain.domains.comment.dto.condition\n\nimport org.springframework.data.domain.Pageable\n\ndata class CommentCondition(\n    val eventId: Long,\n    val pageable: Pageable,\n)\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/comment/exception/CommentAlreadyDeleteException.kt",
    "content": "package band.gosrock.domain.domains.comment.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CommentAlreadyDeleteException : DuDoongCodeException(CommentErrorCode.COMMENT_ALREADY_DELETE) {\n    companion object {\n        val EXCEPTION: DuDoongCodeException = CommentAlreadyDeleteException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/comment/exception/CommentErrorCode.kt",
    "content": "package band.gosrock.domain.domains.comment.exception\n\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.consts.DuDoongStatic.BAD_REQUEST\nimport band.gosrock.common.consts.DuDoongStatic.NOT_FOUND\nimport band.gosrock.common.dto.ErrorReason\nimport band.gosrock.common.exception.BaseErrorCode\nimport java.lang.reflect.Field\n\nenum class CommentErrorCode(\n    private val status: Int,\n    private val code: String,\n    private val reason: String,\n) : BaseErrorCode {\n    COMMENT_NOT_FOUND(NOT_FOUND, \"Comment_404_1\", \"응원글을 찾을 수 없습니다.\"),\n\n    @ExplainError(value = \"eventId 경로변수와 commentId가 맞지 않을 때 발생하는 에러입니다.\")\n    COMMENT_NOT_MATCH_EVENT(BAD_REQUEST, \"Comment_400_1\", \"응원글과 이벤트가 맞지 않습니다.\"),\n\n    COMMENT_ALREADY_DELETE(BAD_REQUEST, \"Comment_400_2\", \"이미 삭제된 응원글입니다.\"),\n    RETRIEVE_RANDOM_COMMENT_NOT_FOUND(NOT_FOUND, \"Comment_404_2\", \"랜덤 응원글을 찾을 수 없습니다.\");\n\n    override fun getErrorReason(): ErrorReason =\n        ErrorReason(status = status, code = code, reason = reason)\n\n    override fun getExplainError(): String {\n        val field: Field = this.javaClass.getField(this.name)\n        val annotation: ExplainError? = field.getAnnotation(ExplainError::class.java)\n        return annotation?.value ?: this.reason\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/comment/exception/CommentNotFoundException.kt",
    "content": "package band.gosrock.domain.domains.comment.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CommentNotFoundException : DuDoongCodeException(CommentErrorCode.COMMENT_NOT_FOUND) {\n    companion object {\n        val EXCEPTION: DuDoongCodeException = CommentNotFoundException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/comment/exception/CommentNotMatchEventException.kt",
    "content": "package band.gosrock.domain.domains.comment.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CommentNotMatchEventException : DuDoongCodeException(CommentErrorCode.COMMENT_NOT_MATCH_EVENT) {\n    companion object {\n        val EXCEPTION: DuDoongCodeException = CommentNotMatchEventException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/comment/exception/RetrieveRandomCommentNotFoundException.kt",
    "content": "package band.gosrock.domain.domains.comment.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass RetrieveRandomCommentNotFoundException : DuDoongCodeException(CommentErrorCode.RETRIEVE_RANDOM_COMMENT_NOT_FOUND) {\n    companion object {\n        val EXCEPTION: DuDoongCodeException = RetrieveRandomCommentNotFoundException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/comment/repository/CommentCustomRepository.kt",
    "content": "package band.gosrock.domain.domains.comment.repository\n\nimport band.gosrock.domain.domains.comment.domain.Comment\nimport band.gosrock.domain.domains.comment.dto.condition.CommentCondition\nimport org.springframework.data.domain.Slice\n\ninterface CommentCustomRepository {\n    fun searchToPage(commentCondition: CommentCondition): Slice<Comment>\n    fun countComment(eventId: Long): Long\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/comment/repository/CommentCustomRepositoryImpl.kt",
    "content": "package band.gosrock.domain.domains.comment.repository\n\nimport band.gosrock.domain.common.util.SliceUtil\nimport band.gosrock.domain.domains.comment.domain.Comment\nimport band.gosrock.domain.domains.comment.domain.CommentStatus\nimport band.gosrock.domain.domains.comment.domain.QComment.comment\nimport band.gosrock.domain.domains.comment.dto.condition.CommentCondition\nimport band.gosrock.domain.domains.user.domain.QUser.user\nimport com.querydsl.core.types.dsl.BooleanExpression\nimport com.querydsl.jpa.impl.JPAQueryFactory\nimport org.springframework.data.domain.Slice\n\nclass CommentCustomRepositoryImpl(\n    private val queryFactory: JPAQueryFactory,\n) : CommentCustomRepository {\n\n    override fun searchToPage(commentCondition: CommentCondition): Slice<Comment> {\n        val comments = queryFactory\n            .selectFrom(comment)\n            .leftJoin(comment.user, user)\n            .fetchJoin()\n            .where(\n                eventIdEq(commentCondition.eventId),\n                comment.commentStatus.eq(CommentStatus.ACTIVE),\n            )\n            .orderBy(comment.id.desc())\n            .offset(commentCondition.pageable.offset)\n            .limit((commentCondition.pageable.pageSize + 1).toLong())\n            .fetch()\n\n        return SliceUtil.valueOf(comments, commentCondition.pageable)\n    }\n\n    private fun eventIdEq(eventId: Long?): BooleanExpression? =\n        eventId?.let { comment.eventId.eq(it) }\n\n    override fun countComment(eventId: Long): Long =\n        queryFactory\n            .select(comment.count())\n            .from(comment)\n            .where(eventIdEq(eventId), comment.commentStatus.eq(CommentStatus.ACTIVE))\n            .fetchOne() ?: 0L\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/comment/repository/CommentRepository.kt",
    "content": "package band.gosrock.domain.domains.comment.repository\n\nimport band.gosrock.domain.domains.comment.domain.Comment\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.jpa.repository.JpaRepository\nimport org.springframework.data.jpa.repository.Query\nimport org.springframework.data.repository.query.Param\n\ninterface CommentRepository : JpaRepository<Comment, Long>, CommentCustomRepository {\n\n    @Query(\n        nativeQuery = true,\n        value = \"SELECT * FROM tbl_comment as c WHERE c.event_id = :eventId ORDER BY RAND() DESC LIMIT :offset\",\n    )\n    fun findAllRandom(@Param(\"eventId\") eventId: Long, @Param(\"offset\") limit: Long): List<Comment>\n\n    /** Admin: 키워드(content, nickName) + eventId 필터로 댓글 검색 */\n    @Query(\n        \"SELECT c FROM tbl_comment c WHERE \" +\n            \"(:keyword IS NULL OR c.content LIKE %:keyword% OR c.nickName LIKE %:keyword%) \" +\n            \"AND (:eventId IS NULL OR c.eventId = :eventId)\"\n    )\n    fun findAllForAdmin(\n        @Param(\"keyword\") keyword: String?,\n        @Param(\"eventId\") eventId: Long?,\n        pageable: Pageable,\n    ): Page<Comment>\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/comment/service/CommentDomainService.kt",
    "content": "package band.gosrock.domain.domains.comment.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.domains.comment.domain.Comment\nimport org.springframework.transaction.annotation.Transactional\n\n@DomainService\nclass CommentDomainService {\n\n    @Transactional\n    fun deleteComment(comment: Comment, eventId: Long) {\n        comment.checkEvent(eventId)\n        comment.delete()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/adaptor/CouponCampaignAdaptor.kt",
    "content": "package band.gosrock.domain.domains.coupon.adaptor\n\nimport band.gosrock.common.annotation.Adaptor\nimport band.gosrock.domain.domains.coupon.domain.CouponCampaign\nimport band.gosrock.domain.domains.coupon.exception.AlreadyExistCouponCampaignException\nimport band.gosrock.domain.domains.coupon.exception.CouponCampaignNotFoundException\nimport band.gosrock.domain.domains.coupon.repository.CouponCampaignRepository\n\n@Adaptor\nclass CouponCampaignAdaptor(\n    private val couponCampaignRepository: CouponCampaignRepository,\n) {\n    fun save(couponCampaign: CouponCampaign): CouponCampaign =\n        couponCampaignRepository.save(couponCampaign)\n\n    fun existsByCouponCode(couponCode: String) {\n        if (couponCampaignRepository.existsByCouponCode(couponCode)) {\n            throw AlreadyExistCouponCampaignException.EXCEPTION\n        }\n    }\n\n    fun findByCouponCode(couponCode: String): CouponCampaign =\n        couponCampaignRepository.findByCouponCode(couponCode)\n            .orElseThrow { CouponCampaignNotFoundException.EXCEPTION }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/adaptor/IssuedCouponAdaptor.kt",
    "content": "package band.gosrock.domain.domains.coupon.adaptor\n\nimport band.gosrock.common.annotation.Adaptor\nimport band.gosrock.domain.domains.coupon.domain.IssuedCoupon\nimport band.gosrock.domain.domains.coupon.exception.AlreadyIssuedCouponException\nimport band.gosrock.domain.domains.coupon.exception.CouponNotFoundException\nimport band.gosrock.domain.domains.coupon.repository.IssuedCouponRepository\n\n@Adaptor\nclass IssuedCouponAdaptor(\n    private val issuedCouponRepository: IssuedCouponRepository,\n) {\n    fun findAllByUserId(userId: Long): List<IssuedCoupon> =\n        issuedCouponRepository.findAllByUserId(userId)\n\n    fun save(issuedCoupon: IssuedCoupon): IssuedCoupon =\n        issuedCouponRepository.save(issuedCoupon)\n\n    fun exist(couponCampaignId: Long, userId: Long) {\n        issuedCouponRepository.findByCouponCampaignIdAndUserId(couponCampaignId, userId)\n            .ifPresent { throw AlreadyIssuedCouponException.EXCEPTION }\n    }\n\n    fun query(issuedCouponId: Long): IssuedCoupon =\n        issuedCouponRepository.findById(issuedCouponId)\n            .orElseThrow { CouponNotFoundException.EXCEPTION }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/domain/ApplyTarget.kt",
    "content": "package band.gosrock.domain.domains.coupon.domain\n\nimport band.gosrock.common.annotation.EnumClass\n\n@EnumClass\nenum class ApplyTarget(val value: String) {\n    ALL(\"ALL\"),\n    SUB(\"SUB\")\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/domain/CouponCampaign.kt",
    "content": "package band.gosrock.domain.domains.coupon.domain\n\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport band.gosrock.domain.common.vo.DateTimePeriod\nimport band.gosrock.domain.domains.coupon.exception.NotIssuingCouponPeriodException\nimport band.gosrock.domain.domains.coupon.exception.WrongDiscountAmountException\nimport java.time.LocalDateTime\nimport jakarta.persistence.CascadeType\nimport jakarta.persistence.Column\nimport jakarta.persistence.Embedded\nimport jakarta.persistence.Entity\nimport jakarta.persistence.EnumType\nimport jakarta.persistence.Enumerated\nimport jakarta.persistence.FetchType\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\nimport jakarta.persistence.OneToMany\nimport org.hibernate.annotations.ColumnDefault\nimport org.hibernate.annotations.DynamicInsert\n\n@DynamicInsert\n@Entity(name = \"tbl_coupon_campaign\")\nclass CouponCampaign(\n    var userId: Long? = null,\n\n    @Enumerated(EnumType.STRING)\n    var discountType: DiscountType? = null,\n\n    @Enumerated(EnumType.STRING)\n    @ColumnDefault(\"'ALL'\")\n    var applyTarget: ApplyTarget? = null,\n\n    var validTerm: Long? = null,\n\n    @Embedded\n    var dateTimePeriod: DateTimePeriod? = null,\n\n    @Embedded\n    var couponStockInfo: CouponStockInfo? = null,\n\n    var discountAmount: Long? = null,\n\n    var couponCode: String? = null,\n\n    var minimumCost: Long? = 10000L,\n) : BaseTimeEntity() {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"coupon_campaign_id\")\n    var id: Long? = null\n        protected set\n\n    @OneToMany(mappedBy = \"couponCampaign\", cascade = [CascadeType.ALL], fetch = FetchType.LAZY)\n    var issuedCoupons: MutableList<IssuedCoupon> = mutableListOf()\n        protected set\n\n    fun validatePercentageAmount(discountType: DiscountType, discountAmount: Long) {\n        if (discountType == DiscountType.PERCENTAGE && discountAmount > 100) {\n            throw WrongDiscountAmountException.EXCEPTION\n        }\n    }\n\n    fun decreaseCouponStock() {\n        couponStockInfo!!.decreaseCouponStock()\n    }\n\n    fun validateIssuePeriod() {\n        val nowTime = LocalDateTime.now()\n        if (!dateTimePeriod!!.contains(nowTime)) {\n            throw NotIssuingCouponPeriodException.EXCEPTION\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/domain/CouponStockInfo.kt",
    "content": "package band.gosrock.domain.domains.coupon.domain\n\nimport band.gosrock.domain.domains.coupon.exception.NoCouponStockLeftException\nimport jakarta.persistence.Embeddable\n\n@Embeddable\nclass CouponStockInfo(\n    var issuedAmount: Long? = null,\n    var remainingAmount: Long? = null,\n) {\n    fun checkCouponLeft() {\n        if (remainingAmount!! < 1) {\n            throw NoCouponStockLeftException.EXCEPTION\n        }\n    }\n\n    fun decreaseCouponStock() {\n        checkCouponLeft()\n        this.remainingAmount = remainingAmount!! - 1\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/domain/DiscountType.kt",
    "content": "package band.gosrock.domain.domains.coupon.domain\n\nimport band.gosrock.common.annotation.EnumClass\n\n@EnumClass\nenum class DiscountType(val value: String) {\n    AMOUNT(\"AMOUNT\"),\n    PERCENTAGE(\"PERCENTAGE\")\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/domain/IssuedCoupon.kt",
    "content": "package band.gosrock.domain.domains.coupon.domain\n\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.coupon.exception.AlreadyRecoveredCouponException\nimport band.gosrock.domain.domains.coupon.exception.AlreadyUsedCouponException\nimport band.gosrock.domain.domains.coupon.exception.NotMyCouponException\nimport band.gosrock.domain.domains.coupon.exception.SupplyLessThenDiscountException\nimport band.gosrock.domain.domains.coupon.exception.SupplyLessThenMinimumException\nimport java.time.LocalDateTime\nimport jakarta.persistence.Column\nimport jakarta.persistence.Entity\nimport jakarta.persistence.FetchType\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\nimport jakarta.persistence.JoinColumn\nimport jakarta.persistence.ManyToOne\n\n@Entity(name = \"tbl_issued_coupon\")\nclass IssuedCoupon(\n    var userId: Long? = null,\n) : BaseTimeEntity() {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"issued_coupon_id\")\n    var id: Long? = null\n        protected set\n\n    var usageStatus: Boolean = false\n        protected set\n\n    @ManyToOne(fetch = FetchType.LAZY)\n    @JoinColumn(name = \"coupon_campaign_id\", nullable = false)\n    var couponCampaign: CouponCampaign? = null\n        protected set\n\n    constructor(couponCampaign: CouponCampaign?, userId: Long?) : this(userId = userId) {\n        this.couponCampaign = couponCampaign\n        this.usageStatus = false\n    }\n\n    fun getIssuedCouponId(): Long? = this.id\n\n    fun getCouponName(): String? = this.couponCampaign?.couponCode\n\n    fun getDiscountAmount(supplyAmount: Money): Money {\n        return if (couponCampaign!!.discountType == DiscountType.AMOUNT) {\n            checkSupplyAmount(supplyAmount, couponCampaign!!.discountAmount!!, couponCampaign!!.minimumCost!!)\n        } else {\n            val discountAmount = supplyAmount.getDiscountAmountByPercentage(supplyAmount, couponCampaign!!.discountAmount!!)\n            checkSupplyAmount(supplyAmount, discountAmount, couponCampaign!!.minimumCost!!)\n        }\n    }\n\n    fun checkSupplyAmount(supply: Money, discount: Long, minimum: Long): Money {\n        if (supply.isLessThan(Money.wons(discount))) {\n            throw SupplyLessThenDiscountException.EXCEPTION\n        }\n        if (supply.isLessThan(Money.wons(minimum))) {\n            throw SupplyLessThenMinimumException.EXCEPTION\n        }\n        return Money.wons(discount)\n    }\n\n    fun validMine(userId: Long) {\n        if (userId != this.userId) {\n            throw NotMyCouponException.EXCEPTION\n        }\n    }\n\n    fun use() {\n        if (usageStatus) {\n            throw AlreadyUsedCouponException.EXCEPTION\n        }\n        usageStatus = true\n    }\n\n    fun recovery() {\n        if (!usageStatus) {\n            throw AlreadyRecoveredCouponException.EXCEPTION\n        }\n        usageStatus = false\n    }\n\n    fun isAvailableTerm(): Boolean =\n        !LocalDateTime.now().isAfter(createdAtKt().plusDays(this.couponCampaign!!.validTerm!!))\n\n    fun calculateValidTerm(): LocalDateTime =\n        createdAtKt().plusDays(this.couponCampaign!!.validTerm!!)\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/exception/AlreadyExistCouponCampaignException.kt",
    "content": "package band.gosrock.domain.domains.coupon.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass AlreadyExistCouponCampaignException private constructor() : DuDoongCodeException(CouponErrorCode.DUPLICATE_COUPON_CODE) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = AlreadyExistCouponCampaignException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/exception/AlreadyIssuedCouponException.kt",
    "content": "package band.gosrock.domain.domains.coupon.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass AlreadyIssuedCouponException private constructor() : DuDoongCodeException(CouponErrorCode.ALREADY_ISSUED_COUPON) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = AlreadyIssuedCouponException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/exception/AlreadyRecoveredCouponException.kt",
    "content": "package band.gosrock.domain.domains.coupon.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass AlreadyRecoveredCouponException private constructor() : DuDoongCodeException(CouponErrorCode.ALREADY_RECOVERED_COUPON) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = AlreadyRecoveredCouponException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/exception/AlreadyUsedCouponException.kt",
    "content": "package band.gosrock.domain.domains.coupon.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass AlreadyUsedCouponException private constructor() : DuDoongCodeException(CouponErrorCode.ALREADY_USED_COUPON) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = AlreadyUsedCouponException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/exception/CouponCampaignNotFoundException.kt",
    "content": "package band.gosrock.domain.domains.coupon.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CouponCampaignNotFoundException private constructor() : DuDoongCodeException(CouponErrorCode.NOT_FOUND_COUPON_CAMPAIGN) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CouponCampaignNotFoundException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/exception/CouponErrorCode.kt",
    "content": "package band.gosrock.domain.domains.coupon.exception\n\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.consts.DuDoongStatic.BAD_REQUEST\nimport band.gosrock.common.consts.DuDoongStatic.NOT_FOUND\nimport band.gosrock.common.dto.ErrorReason\nimport band.gosrock.common.exception.BaseErrorCode\nimport java.lang.reflect.Field\n\nenum class CouponErrorCode(\n    private val status: Int,\n    private val code: String,\n    private val reason: String,\n) : BaseErrorCode {\n    DUPLICATE_COUPON_CODE(BAD_REQUEST, \"Coupon_400_1\", \"동일한 쿠폰 코드가 이미 존재합니다.\"),\n    WRONG_DISCOUNT_AMOUNT(BAD_REQUEST, \"Coupon_400_2\", \"정률 할인은 100 이하 퍼센트만 할인 가능합니다.\"),\n    NOT_FOUND_COUPON_CAMPAIGN(NOT_FOUND, \"Coupon_404_1\", \"존재하지 않는 쿠폰 캠페인입니다.\"),\n    ALREADY_ISSUED_COUPON(BAD_REQUEST, \"Coupon_400_4\", \"이미 발급된 쿠폰입니다.\"),\n    NO_COUPON_STOCK_LEFT(BAD_REQUEST, \"Coupon_400_5\", \"쿠폰이 모두 소진됐습니다.\"),\n    NOT_COUPON_ISSUING_PERIOD(BAD_REQUEST, \"Coupon_400_6\", \"쿠폰 발급 가능 시각이 아닙니다.\"),\n    NOT_FOUND_COUPON(NOT_FOUND, \"Coupon_404_2\", \"존재하지 않는 쿠폰 입니다.\"),\n    NOT_MY_COUPON(BAD_REQUEST, \"Coupon_400_7\", \"내 쿠폰이 아닙니다.\"),\n    ALREADY_USED_COUPON(BAD_REQUEST, \"Coupon_400_8\", \"이미 사용한 쿠폰입니다.\"),\n    SUPPLY_LESS_THEN_DISCOUNT(BAD_REQUEST, \"Coupon_400_9\", \"적용 불가 쿠폰입니다. 결제 금액이 할인 금액보다 작습니다.\"),\n    SUPPLY_LESS_THEN_MINIMUM(BAD_REQUEST, \"Coupon_400_10\", \"적용 불가 쿠폰입니다. 결제 금액이 최소 결제 금액보다 작습니다.\"),\n    ALREADY_RECOVERED_COUPON(BAD_REQUEST, \"Coupon_400_11\", \"이미 복구한 쿠폰입니다.\");\n\n    override fun getErrorReason(): ErrorReason =\n        ErrorReason(status = status, code = code, reason = reason)\n\n    override fun getExplainError(): String {\n        val field: Field = this.javaClass.getField(this.name)\n        val annotation: ExplainError? = field.getAnnotation(ExplainError::class.java)\n        return annotation?.value ?: this.reason\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/exception/CouponNotFoundException.kt",
    "content": "package band.gosrock.domain.domains.coupon.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CouponNotFoundException private constructor() : DuDoongCodeException(CouponErrorCode.NOT_FOUND_COUPON) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CouponNotFoundException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/exception/NoCouponStockLeftException.kt",
    "content": "package band.gosrock.domain.domains.coupon.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass NoCouponStockLeftException private constructor() : DuDoongCodeException(CouponErrorCode.NO_COUPON_STOCK_LEFT) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = NoCouponStockLeftException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/exception/NotIssuingCouponPeriodException.kt",
    "content": "package band.gosrock.domain.domains.coupon.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass NotIssuingCouponPeriodException private constructor() : DuDoongCodeException(CouponErrorCode.NOT_COUPON_ISSUING_PERIOD) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = NotIssuingCouponPeriodException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/exception/NotMyCouponException.kt",
    "content": "package band.gosrock.domain.domains.coupon.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass NotMyCouponException private constructor() : DuDoongCodeException(CouponErrorCode.NOT_MY_COUPON) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = NotMyCouponException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/exception/SupplyLessThenDiscountException.kt",
    "content": "package band.gosrock.domain.domains.coupon.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass SupplyLessThenDiscountException private constructor() : DuDoongCodeException(CouponErrorCode.SUPPLY_LESS_THEN_DISCOUNT) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = SupplyLessThenDiscountException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/exception/SupplyLessThenMinimumException.kt",
    "content": "package band.gosrock.domain.domains.coupon.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass SupplyLessThenMinimumException private constructor() : DuDoongCodeException(CouponErrorCode.SUPPLY_LESS_THEN_MINIMUM) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = SupplyLessThenMinimumException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/exception/WrongDiscountAmountException.kt",
    "content": "package band.gosrock.domain.domains.coupon.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass WrongDiscountAmountException private constructor() : DuDoongCodeException(CouponErrorCode.WRONG_DISCOUNT_AMOUNT) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = WrongDiscountAmountException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/repository/CouponCampaignRepository.kt",
    "content": "package band.gosrock.domain.domains.coupon.repository\n\nimport band.gosrock.domain.domains.coupon.domain.CouponCampaign\nimport org.springframework.data.jpa.repository.JpaRepository\nimport java.util.Optional\n\ninterface CouponCampaignRepository : JpaRepository<CouponCampaign, Long> {\n    fun existsByCouponCode(couponCode: String): Boolean\n    fun findByCouponCode(couponCode: String): Optional<CouponCampaign>\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/repository/IssuedCouponCustomRepository.kt",
    "content": "package band.gosrock.domain.domains.coupon.repository\n\nimport band.gosrock.domain.domains.coupon.domain.IssuedCoupon\n\ninterface IssuedCouponCustomRepository {\n    fun findAllByUserId(userId: Long): List<IssuedCoupon>\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/repository/IssuedCouponCustomRepositoryImpl.kt",
    "content": "package band.gosrock.domain.domains.coupon.repository\n\nimport band.gosrock.domain.domains.coupon.domain.IssuedCoupon\nimport band.gosrock.domain.domains.coupon.domain.QIssuedCoupon.issuedCoupon\nimport com.querydsl.core.types.dsl.BooleanExpression\nimport com.querydsl.jpa.impl.JPAQueryFactory\n\nclass IssuedCouponCustomRepositoryImpl(\n    private val queryFactory: JPAQueryFactory\n) : IssuedCouponCustomRepository {\n\n    override fun findAllByUserId(userId: Long): List<IssuedCoupon> =\n        queryFactory.selectFrom(issuedCoupon).where(userIdEq(userId)).fetch()\n\n    private fun userIdEq(userId: Long?): BooleanExpression? =\n        if (userId == null) null else issuedCoupon.userId.eq(userId)\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/repository/IssuedCouponRepository.kt",
    "content": "package band.gosrock.domain.domains.coupon.repository\n\nimport band.gosrock.domain.domains.coupon.domain.IssuedCoupon\nimport org.springframework.data.jpa.repository.JpaRepository\nimport java.util.Optional\n\ninterface IssuedCouponRepository : JpaRepository<IssuedCoupon, Long>, IssuedCouponCustomRepository {\n    fun findByCouponCampaignIdAndUserId(couponCampaignId: Long, userId: Long): Optional<IssuedCoupon>\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/service/CreateCouponCampaignDomainService.kt",
    "content": "package band.gosrock.domain.domains.coupon.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.domains.coupon.adaptor.CouponCampaignAdaptor\nimport band.gosrock.domain.domains.coupon.domain.CouponCampaign\nimport org.springframework.transaction.annotation.Transactional\n\n@DomainService\n@Transactional(readOnly = true)\nclass CreateCouponCampaignDomainService(\n    private val couponCampaignAdaptor: CouponCampaignAdaptor,\n) {\n    fun checkCouponCodeExists(couponCode: String) {\n        couponCampaignAdaptor.existsByCouponCode(couponCode)\n    }\n\n    @Transactional\n    fun createCouponCampaign(couponCampaign: CouponCampaign): CouponCampaign {\n        couponCampaign.validatePercentageAmount(couponCampaign.discountType!!, couponCampaign.discountAmount!!)\n        return couponCampaignAdaptor.save(couponCampaign)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/service/CreateIssuedCouponDomainService.kt",
    "content": "package band.gosrock.domain.domains.coupon.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.common.aop.redissonLock.RedissonLock\nimport band.gosrock.domain.domains.coupon.adaptor.IssuedCouponAdaptor\nimport band.gosrock.domain.domains.coupon.domain.CouponCampaign\nimport band.gosrock.domain.domains.coupon.domain.IssuedCoupon\n\n@DomainService\nclass CreateIssuedCouponDomainService(\n    private val issuedCouponAdaptor: IssuedCouponAdaptor,\n) {\n    @RedissonLock(LockName = \"유저쿠폰발급\", identifier = \"id\", paramClassType = CouponCampaign::class)\n    fun createIssuedCoupon(issuedCoupon: IssuedCoupon, couponCampaign: CouponCampaign): IssuedCoupon {\n        issuedCouponAdaptor.exist(couponCampaign.id!!, issuedCoupon.userId!!)\n        couponCampaign.validateIssuePeriod()\n        couponCampaign.decreaseCouponStock()\n        return issuedCouponAdaptor.save(issuedCoupon)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/service/RecoveryCouponService.kt",
    "content": "package band.gosrock.domain.domains.coupon.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.common.aop.redissonLock.RedissonLock\nimport band.gosrock.domain.domains.coupon.adaptor.IssuedCouponAdaptor\n\n@DomainService\nclass RecoveryCouponService(\n    private val issuedCouponAdaptor: IssuedCouponAdaptor,\n) {\n    @RedissonLock(LockName = \"쿠폰\", identifier = \"couponId\")\n    fun execute(userId: Long, issuedCouponId: Long): Long {\n        val coupon = issuedCouponAdaptor.query(issuedCouponId)\n        coupon.validMine(userId)\n        coupon.recovery()\n        return issuedCouponId\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/service/UseCouponService.kt",
    "content": "package band.gosrock.domain.domains.coupon.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.common.aop.redissonLock.RedissonLock\nimport band.gosrock.domain.domains.coupon.adaptor.IssuedCouponAdaptor\n\n@DomainService\nclass UseCouponService(\n    private val issuedCouponAdaptor: IssuedCouponAdaptor,\n) {\n    @RedissonLock(LockName = \"쿠폰\", identifier = \"couponId\")\n    fun execute(userId: Long, issuedCouponId: Long): Long {\n        val coupon = issuedCouponAdaptor.query(issuedCouponId)\n        coupon.validMine(userId)\n        coupon.use()\n        return issuedCouponId\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/service/handler/CreateOrderCouponHandler.kt",
    "content": "package band.gosrock.domain.domains.coupon.service.handler\n\nimport band.gosrock.domain.common.events.order.CreateOrderEvent\nimport band.gosrock.domain.domains.coupon.service.UseCouponService\nimport org.slf4j.LoggerFactory\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\n@Component\nclass CreateOrderCouponHandler(\n    private val useCouponService: UseCouponService,\n) {\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    @TransactionalEventListener(classes = [CreateOrderEvent::class], phase = TransactionPhase.BEFORE_COMMIT)\n    fun handleDoneOrderFailEvent(createOrderEvent: CreateOrderEvent) {\n        log.info(\"${createOrderEvent.orderUuid} 주문 생성 이벤트 쿠폰 사용 리스너\")\n        if (createOrderEvent.isUsingCoupon) {\n            log.info(\"${createOrderEvent.orderUuid} 주문 생성 이벤트 쿠폰 사용 리스너 : 쿠폰 사용 도메인 서비스 호출\")\n            useCouponService.execute(createOrderEvent.userId, createOrderEvent.issuedCouponId!!)\n            log.info(\"${createOrderEvent.orderUuid} 주문 생성 이벤트 쿠폰 사용 리스너 : 쿠폰 사용 도메인 서비스 호출 종료\")\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/coupon/service/handler/WithDrawOrderCouponHandler.kt",
    "content": "package band.gosrock.domain.domains.coupon.service.handler\n\nimport band.gosrock.domain.common.events.order.WithDrawOrderEvent\nimport band.gosrock.domain.domains.coupon.service.RecoveryCouponService\nimport org.slf4j.LoggerFactory\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\n@Component\nclass WithDrawOrderCouponHandler(\n    private val recoveryCouponService: RecoveryCouponService,\n) {\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    @TransactionalEventListener(classes = [WithDrawOrderEvent::class], phase = TransactionPhase.BEFORE_COMMIT)\n    fun handleWithDrawOrderEvent(withDrawOrderEvent: WithDrawOrderEvent) {\n        log.info(\"${withDrawOrderEvent.orderUuid} 주문 철회 이벤트 쿠폰 회복 리스너\")\n        if (withDrawOrderEvent.isUsingCoupon) {\n            log.info(\"${withDrawOrderEvent.orderUuid} 주문 철회 이벤트 쿠폰 회복 리스너 : 쿠폰 회복 도메인 서비스 호출\")\n            recoveryCouponService.execute(withDrawOrderEvent.userId, withDrawOrderEvent.issuedCouponId!!)\n            log.info(\"${withDrawOrderEvent.orderUuid} 주문 철회 이벤트 쿠폰 사용 리스너 : 쿠폰 회복 도메인 서비스 호출 종료\")\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/adaptor/EventAdaptor.kt",
    "content": "package band.gosrock.domain.domains.event.adaptor\n\nimport band.gosrock.common.annotation.Adaptor\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.event.domain.EventStatus\nimport band.gosrock.domain.domains.event.exception.EventNotFoundException\nimport band.gosrock.domain.domains.event.repository.EventRepository\nimport java.time.LocalDateTime\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.domain.Slice\n\n@Adaptor\nclass EventAdaptor(private val eventRepository: EventRepository) {\n\n    fun findById(eventId: Long): Event =\n        eventRepository.findById(eventId).orElseThrow { EventNotFoundException.EXCEPTION }\n\n    fun findAllByHostId(hostId: Long, pageable: Pageable): Page<Event> =\n        eventRepository.findAllByHostId(hostId, pageable)\n\n    fun findAllByHostIdIn(hostId: List<Long>, pageable: Pageable): Page<Event> =\n        eventRepository.findAllByHostIdIn(hostId, pageable)\n\n    fun querySliceEventsByHostIdIn(hostId: List<Long>, pageable: Pageable): Slice<Event> =\n        eventRepository.querySliceEventsByHostIdIn(hostId, pageable)\n\n    fun querySliceEventsByStatus(status: EventStatus, pageable: Pageable): Slice<Event> =\n        eventRepository.querySliceEventsByStatus(status, pageable)\n\n    fun querySliceEventsByKeyword(keyword: String?, pageable: Pageable): Slice<Event> =\n        eventRepository.querySliceEventsByKeyword(keyword, pageable)\n\n    fun queryEventsByEndAtBeforeAndStatusOpen(time: LocalDateTime): List<Event> =\n        eventRepository.queryEventsByEndAtBeforeAndStatusOpen(time)\n\n    fun findAllByIds(ids: List<Long>): List<Event> =\n        eventRepository.findAllByIdIn(ids)\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/domain/Event.kt",
    "content": "package band.gosrock.domain.domains.event.domain\n\nimport band.gosrock.common.exception.DuDoongCodeException\nimport band.gosrock.domain.common.aop.domainEvent.Events\nimport band.gosrock.domain.common.events.event.EventContentChangeEvent\nimport band.gosrock.domain.common.events.event.EventCreationEvent\nimport band.gosrock.domain.common.events.event.EventDeletionEvent\nimport band.gosrock.domain.common.events.event.EventStatusChangeEvent\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport band.gosrock.domain.common.vo.EventBasicVo\nimport band.gosrock.domain.common.vo.EventDetailVo\nimport band.gosrock.domain.common.vo.EventInfoVo\nimport band.gosrock.domain.common.vo.EventPlaceVo\nimport band.gosrock.domain.common.vo.EventProfileVo\nimport band.gosrock.domain.common.vo.RefundInfoVo\nimport band.gosrock.domain.domains.event.domain.EventStatus.CALCULATING\nimport band.gosrock.domain.domains.event.domain.EventStatus.CLOSED\nimport band.gosrock.domain.domains.event.domain.EventStatus.DELETED\nimport band.gosrock.domain.domains.event.domain.EventStatus.OPEN\nimport band.gosrock.domain.domains.event.domain.EventStatus.PREPARING\nimport band.gosrock.domain.domains.event.exception.AlreadyCalculatingStatusException\nimport band.gosrock.domain.domains.event.exception.AlreadyCloseStatusException\nimport band.gosrock.domain.domains.event.exception.AlreadyDeletedStatusException\nimport band.gosrock.domain.domains.event.exception.AlreadyOpenStatusException\nimport band.gosrock.domain.domains.event.exception.AlreadyPreparingStatusException\nimport band.gosrock.domain.domains.event.exception.CannotDeleteByOpenEventException\nimport band.gosrock.domain.domains.event.exception.CannotModifyOpenEventException\nimport band.gosrock.domain.domains.event.exception.EventNotOpenException\nimport band.gosrock.domain.domains.event.exception.EventOpenTimeExpiredException\nimport band.gosrock.domain.domains.event.exception.InvalidEventStatusTransitionException\nimport band.gosrock.domain.domains.event.exception.EventTicketingTimeIsPassedException\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport java.time.LocalDateTime\nimport jakarta.persistence.Column\nimport jakarta.persistence.Embedded\nimport jakarta.persistence.Entity\nimport jakarta.persistence.EnumType\nimport jakarta.persistence.Enumerated\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\nimport org.hibernate.annotations.Where\n\n@Where(clause = \"status != 'DELETED'\")\n@Entity(name = \"tbl_event\")\nclass Event(\n    // 호스트 정보\n    var hostId: Long? = null,\n    name: String? = null,\n    startAt: LocalDateTime? = null,\n    runTime: Long? = null,\n) : BaseTimeEntity() {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"event_id\")\n    var id: Long? = null\n        protected set\n\n    @Embedded\n    var eventBasic: EventBasic? = if (name != null || startAt != null || runTime != null) {\n        EventBasic(name = name, startAt = startAt, runTime = runTime)\n    } else null\n        protected set\n\n    @Embedded\n    var eventPlace: EventPlace? = null\n        protected set\n\n    @Embedded\n    var eventDetail: EventDetail? = null\n        protected set\n\n    // 이벤트 상태\n    @Enumerated(EnumType.STRING)\n    var status: EventStatus = PREPARING\n        protected set\n\n    init {\n        if (hostId != null && name != null) {\n            Events.raise(EventCreationEvent.of(hostId, name))\n        }\n    }\n\n    fun getStartAt(): LocalDateTime? = this.eventBasic?.startAt\n\n    fun getEndAt(): LocalDateTime? = this.eventBasic?.endAt()\n\n    fun hasEventBasic(): Boolean = this.eventBasic?.isUpdated() == true\n\n    fun hasEventPlace(): Boolean = this.eventPlace?.isUpdated() == true\n\n    fun hasEventDetail(): Boolean = this.eventDetail?.isUpdated() == true\n\n    fun isPreparing(): Boolean = this.status == PREPARING\n\n    fun isClosed(): Boolean = this.status == CLOSED\n\n    fun updateEventBasic(eventBasic: EventBasic?) {\n        validateOpenStatus()\n        this.eventBasic = eventBasic\n    }\n\n    fun updateEventDetail(eventDetail: EventDetail) {\n        this.eventDetail = eventDetail\n        Events.raise(EventContentChangeEvent.of(this))\n    }\n\n    fun updateEventPlace(eventPlace: EventPlace) {\n        validateOpenStatus()\n        this.eventPlace = eventPlace\n    }\n\n    fun validateStartAt() {\n        val startAt = getStartAt() ?: throw IllegalStateException(\"Event startAt must be set\")\n        if (startAt.isBefore(LocalDateTime.now())) throw EventOpenTimeExpiredException.EXCEPTION\n    }\n\n    fun validateOpenStatus() {\n        if (status == OPEN) throw CannotModifyOpenEventException.EXCEPTION\n    }\n\n    fun validateNotOpenStatus() {\n        if (status != OPEN) throw EventNotOpenException.EXCEPTION\n    }\n\n    fun validateTicketingTime() {\n        if (!isTimeBeforeStartAt()) throw EventTicketingTimeIsPassedException.EXCEPTION\n    }\n\n    fun isRefundDateNotPassed(): Boolean = toRefundInfoVo().availAble\n\n    fun isTimeBeforeStartAt(): Boolean {\n        val startAt = getStartAt() ?: throw IllegalStateException(\"Event startAt must be set\")\n        return LocalDateTime.now().isBefore(startAt)\n    }\n\n    fun toRefundInfoVoWithOrderStatus(orderStatus: OrderStatus): RefundInfoVo {\n        val startAt = getStartAt() ?: throw IllegalStateException(\"Event startAt must be set\")\n        return RefundInfoVo.of(startAt, orderStatus)\n    }\n\n    fun toRefundInfoVo(): RefundInfoVo {\n        val startAt = getStartAt() ?: throw IllegalStateException(\"Event startAt must be set\")\n        return RefundInfoVo.from(startAt)\n    }\n\n    fun toEventInfoVo(): EventInfoVo = EventInfoVo.from(this)\n\n    fun toEventDetailVo(): EventDetailVo = EventDetailVo.from(this)\n\n    fun toEventBasicVo(): EventBasicVo = EventBasicVo.from(this)\n\n    fun toEventProfileVo(): EventProfileVo = EventProfileVo.from(this)\n\n    fun toEventPlaceVo(): EventPlaceVo = EventPlaceVo.from(this)\n\n    fun prepare() {\n        updateStatus(PREPARING, AlreadyPreparingStatusException.EXCEPTION)\n    }\n\n    fun open() {\n        validateStartAt()\n        updateStatus(OPEN, AlreadyOpenStatusException.EXCEPTION)\n    }\n\n    fun calculate() {\n        updateStatus(CALCULATING, AlreadyCalculatingStatusException.EXCEPTION)\n    }\n\n    fun close() {\n        updateStatus(CLOSED, AlreadyCloseStatusException.EXCEPTION)\n    }\n\n    private fun updateStatus(newStatus: EventStatus, alreadySameException: DuDoongCodeException) {\n        if (this.status == newStatus) throw alreadySameException\n        if (!this.status.canTransitionTo(newStatus)) {\n            throw InvalidEventStatusTransitionException.EXCEPTION\n        }\n        this.status = newStatus\n        Events.raise(EventStatusChangeEvent.of(this))\n    }\n\n    fun deleteSoft() {\n        // 오픈된 이벤트는 삭제 불가\n        if (this.status == OPEN) throw CannotDeleteByOpenEventException.EXCEPTION\n        if (this.status == DELETED) throw AlreadyDeletedStatusException.EXCEPTION\n        this.status = DELETED\n        Events.raise(EventDeletionEvent.of(this))\n    }\n\n    fun getEventName(): String? = eventBasic?.name\n\n    /** 어드민 전용: 상태 전이 밸리데이션 없이 직접 상태 변경 */\n    fun adminUpdateStatus(newStatus: EventStatus) {\n        this.status = newStatus\n    }\n\n    /** 어드민 전용: OPEN 여부 무관하게 기본 정보 부분 수정 */\n    fun adminUpdate(\n        name: String?,\n        startAt: java.time.LocalDateTime?,\n        runTime: Long?,\n        content: String?,\n        placeName: String?,\n        placeAddress: String?,\n    ) {\n        val currentBasic = this.eventBasic\n        this.eventBasic = EventBasic(\n            name = name ?: currentBasic?.name,\n            startAt = startAt ?: currentBasic?.startAt,\n            runTime = runTime ?: currentBasic?.runTime,\n        )\n        if (content != null) {\n            val currentDetail = this.eventDetail\n            this.eventDetail = EventDetail(\n                posterImageKey = currentDetail?.posterImage?.imageKey,\n                content = content,\n            )\n        }\n        if (placeName != null || placeAddress != null) {\n            val currentPlace = this.eventPlace\n            this.eventPlace = EventPlace(\n                latitude = currentPlace?.latitude,\n                longitude = currentPlace?.longitude,\n                placeName = placeName ?: currentPlace?.placeName,\n                placeAddress = placeAddress ?: currentPlace?.placeAddress,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/domain/EventBasic.kt",
    "content": "package band.gosrock.domain.domains.event.domain\n\nimport java.time.LocalDateTime\nimport jakarta.persistence.Column\nimport jakarta.persistence.Embeddable\n\n@Embeddable\nclass EventBasic(\n    @Column(length = 25)\n    var name: String? = null,\n\n    var startAt: LocalDateTime? = null,\n\n    var runTime: Long? = null,\n) {\n    fun isUpdated(): Boolean = this.name != null && this.startAt != null && this.runTime != null\n\n    fun endAt(): LocalDateTime? = if (this.runTime == null) null else this.startAt?.plusMinutes(this.runTime!!)\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/domain/EventDetail.kt",
    "content": "package band.gosrock.domain.domains.event.domain\n\nimport band.gosrock.domain.common.vo.ImageVo\nimport jakarta.persistence.Column\nimport jakarta.persistence.Embeddable\nimport jakarta.persistence.Embedded\n\n@Embeddable\nclass EventDetail(\n    posterImageKey: String? = null,\n\n    // (마크다운) 공연 상세 내용\n    @Column(columnDefinition = \"TEXT\")\n    var content: String? = null,\n) {\n    // 포스터 이미지\n    @Embedded\n    var posterImage: ImageVo? = posterImageKey?.let { ImageVo.valueOf(it) }\n\n    fun isUpdated(): Boolean = this.posterImage != null && this.content != null\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/domain/EventDetailImage.kt",
    "content": "package band.gosrock.domain.domains.event.domain\n\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport jakarta.persistence.Column\nimport jakarta.persistence.Entity\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\n\n@Entity(name = \"tbl_event_detail_image\")\nclass EventDetailImage(\n    // 이벤트 정보\n    var eventId: Long? = null,\n    // 이미지 주소\n    var imageUrl: String? = null,\n) : BaseTimeEntity() {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"event_detail_image_id\")\n    var id: Long? = null\n        protected set\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/domain/EventPlace.kt",
    "content": "package band.gosrock.domain.domains.event.domain\n\nimport jakarta.persistence.Embeddable\n\n@Embeddable\nclass EventPlace(\n    // (지도 정보) 위도 - x\n    var latitude: Double? = null,\n    // (지도 정보) 경도 - y\n    var longitude: Double? = null,\n    // 공연 장소\n    var placeName: String? = null,\n    // 공연 상세 주소\n    var placeAddress: String? = null,\n) {\n    fun isUpdated(): Boolean =\n        this.latitude != null && this.longitude != null && this.placeName != null && this.placeAddress != null\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/domain/EventStatus.kt",
    "content": "package band.gosrock.domain.domains.event.domain\n\nimport band.gosrock.common.annotation.EnumClass\nimport com.fasterxml.jackson.annotation.JsonValue\n\n@EnumClass\nenum class EventStatus(\n    val statusName: String,\n    @JsonValue val value: String,\n) {\n    PREPARING(\"PREPARING\", \"준비중\"),\n    OPEN(\"OPEN\", \"진행중\"),\n    CALCULATING(\"CALCULATING\", \"정산중\"),\n    CLOSED(\"CLOSED\", \"지난공연\"),\n    DELETED(\"DELETED\", \"삭제된공연\");\n\n    fun canTransitionTo(newStatus: EventStatus): Boolean =\n        when (this) {\n            PREPARING -> newStatus in setOf(OPEN, DELETED)\n            OPEN -> newStatus in setOf(CALCULATING, DELETED)\n            CALCULATING -> newStatus == CLOSED\n            CLOSED, DELETED -> false\n        }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/AlreadyCalculatingStatusException.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass AlreadyCalculatingStatusException private constructor() : DuDoongCodeException(EventErrorCode.ALREADY_CALCULATING_STATUS) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = AlreadyCalculatingStatusException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/AlreadyCloseStatusException.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass AlreadyCloseStatusException private constructor() : DuDoongCodeException(EventErrorCode.ALREADY_CLOSE_STATUS) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = AlreadyCloseStatusException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/AlreadyDeletedStatusException.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass AlreadyDeletedStatusException private constructor() : DuDoongCodeException(EventErrorCode.ALREADY_DELETED_STATUS) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = AlreadyDeletedStatusException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/AlreadyExistEventUrlNameException.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass AlreadyExistEventUrlNameException private constructor() : DuDoongCodeException(EventErrorCode.EVENT_URL_NAME_ALREADY_EXIST) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = AlreadyExistEventUrlNameException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/AlreadyOpenStatusException.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass AlreadyOpenStatusException private constructor() : DuDoongCodeException(EventErrorCode.ALREADY_OPEN_STATUS) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = AlreadyOpenStatusException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/AlreadyPreparingStatusException.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass AlreadyPreparingStatusException private constructor() : DuDoongCodeException(EventErrorCode.ALREADY_PREPARING_STATUS) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = AlreadyPreparingStatusException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/CannotDeleteByIssuedTicketException.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CannotDeleteByIssuedTicketException private constructor() : DuDoongCodeException(EventErrorCode.CANNOT_DELETE_BY_ISSUED_TICKET) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CannotDeleteByIssuedTicketException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/CannotDeleteByOpenEventException.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CannotDeleteByOpenEventException private constructor() : DuDoongCodeException(EventErrorCode.CANNOT_DELETE_BY_OPEN_EVENT) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CannotDeleteByOpenEventException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/CannotModifyOpenEventException.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CannotModifyOpenEventException private constructor() : DuDoongCodeException(EventErrorCode.CANNOT_MODIFY_OPEN_EVENT) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CannotModifyOpenEventException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/CannotOpenEventException.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CannotOpenEventException private constructor() : DuDoongCodeException(EventErrorCode.CANNOT_OPEN_EVENT) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CannotOpenEventException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/EventCannotEndBeforeStartException.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass EventCannotEndBeforeStartException private constructor() : DuDoongCodeException(EventErrorCode.EVENT_CANNOT_END_BEFORE_START) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = EventCannotEndBeforeStartException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/EventErrorCode.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.consts.DuDoongStatic.BAD_REQUEST\nimport band.gosrock.common.consts.DuDoongStatic.NOT_FOUND\nimport band.gosrock.common.dto.ErrorReason\nimport band.gosrock.common.exception.BaseErrorCode\nimport java.lang.reflect.Field\n\nenum class EventErrorCode(\n    private val status: Int,\n    private val code: String,\n    private val reason: String,\n) : BaseErrorCode {\n    EVENT_NOT_FOUND(NOT_FOUND, \"Event_404_1\", \"이벤트를 찾을 수 없습니다.\"),\n\n    HOST_NOT_AUTH_EVENT(BAD_REQUEST, \"Event_400_1\", \"Host Not Auth Event.\"),\n    EVENT_CANNOT_END_BEFORE_START(BAD_REQUEST, \"Event_400_2\", \"시작 시각은 종료 시각보다 빨라야 합니다.\"),\n    EVENT_URL_NAME_ALREADY_EXIST(BAD_REQUEST, \"Event_400_3\", \"중복된 URL 표시 이름입니다.\"),\n    CANNOT_MODIFY_OPEN_EVENT(BAD_REQUEST, \"Event_400_4\", \"오픈된 이벤트 정보는 수정할 수 없습니다.\"),\n    EVENT_NOT_OPEN(BAD_REQUEST, \"Event_400_5\", \"아직 오픈되지 않은 이벤트에는 접근할 수 없습니다.\"),\n    EVENT_TICKETING_TIME_IS_PASSED(BAD_REQUEST, \"Event_400_6\", \"이벤트 시작시간이 지나 티켓팅을 할 수 없습니다.\"),\n    CANNOT_OPEN_EVENT(BAD_REQUEST, \"Event_400_7\", \"이벤트 오픈 조건을 충족하지 않았습니다.\"),\n    ALREADY_OPEN_STATUS(BAD_REQUEST, \"Event_400_8\", \"이미 오픈 중인 이벤트입니다.\"),\n    ALREADY_CALCULATING_STATUS(BAD_REQUEST, \"Event_400_9\", \"이미 정산중인 이벤트입니다.\"),\n    ALREADY_CLOSE_STATUS(BAD_REQUEST, \"Event_400_10\", \"이미 닫은 이벤트입니다.\"),\n    ALREADY_PREPARING_STATUS(BAD_REQUEST, \"Event_400_11\", \"이미 준비중인 이벤트입니다.\"),\n    ALREADY_DELETED_STATUS(BAD_REQUEST, \"Event_400_12\", \"이미 삭제된 이벤트입니다.\"),\n    CANNOT_DELETE_BY_ISSUED_TICKET(BAD_REQUEST, \"Event_400_13\", \"발급 티켓이 있는 이벤트는 삭제할 수 없습니다.\"),\n    CANNOT_DELETE_BY_OPEN_EVENT(BAD_REQUEST, \"Event_400_14\", \"오픈 상태인 이벤트는 삭제할 수 없습니다.\"),\n    OPEN_TIME_EXPIRED(BAD_REQUEST, \"Event_400_15\", \"오픈 예정 시간이 현재 시간보다 빠릅니다.\"),\n\n    INVALID_EVENT_STATUS_TRANSITION(BAD_REQUEST, \"Event_400_16\", \"허용되지 않는 상태 전이입니다.\"),\n\n    USE_OTHER_API(BAD_REQUEST, \"Event_400_8\", \"잘못된 접근입니다.\");\n\n    override fun getErrorReason(): ErrorReason =\n        ErrorReason(status = status, code = code, reason = reason)\n\n    override fun getExplainError(): String {\n        val field: Field = this.javaClass.getField(this.name)\n        val annotation: ExplainError? = field.getAnnotation(ExplainError::class.java)\n        return annotation?.value ?: this.reason\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/EventNotFoundException.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass EventNotFoundException private constructor() : DuDoongCodeException(EventErrorCode.EVENT_NOT_FOUND) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = EventNotFoundException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/EventNotOpenException.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass EventNotOpenException private constructor() : DuDoongCodeException(EventErrorCode.EVENT_NOT_OPEN) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = EventNotOpenException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/EventOpenTimeExpiredException.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass EventOpenTimeExpiredException private constructor() : DuDoongCodeException(EventErrorCode.OPEN_TIME_EXPIRED) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = EventOpenTimeExpiredException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/EventTicketingTimeIsPassedException.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass EventTicketingTimeIsPassedException private constructor() : DuDoongCodeException(EventErrorCode.EVENT_TICKETING_TIME_IS_PASSED) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = EventTicketingTimeIsPassedException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/HostNotAuthEventException.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass HostNotAuthEventException private constructor() : DuDoongCodeException(EventErrorCode.HOST_NOT_AUTH_EVENT) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = HostNotAuthEventException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/InvalidEventStatusTransitionException.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass InvalidEventStatusTransitionException private constructor() : DuDoongCodeException(EventErrorCode.INVALID_EVENT_STATUS_TRANSITION) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = InvalidEventStatusTransitionException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/exception/UseOtherApiException.kt",
    "content": "package band.gosrock.domain.domains.event.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass UseOtherApiException private constructor() : DuDoongCodeException(EventErrorCode.USE_OTHER_API) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = UseOtherApiException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/repository/EventCustomRepository.kt",
    "content": "package band.gosrock.domain.domains.event.repository\n\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.event.domain.EventStatus\nimport java.time.LocalDateTime\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.domain.Slice\n\ninterface EventCustomRepository {\n    fun querySliceEventsByHostIdIn(hostIds: List<Long>, pageable: Pageable): Slice<Event>\n\n    fun querySliceEventsByStatus(status: EventStatus, pageable: Pageable): Slice<Event>\n\n    fun querySliceEventsByKeyword(keyword: String?, pageable: Pageable): Slice<Event>\n\n    fun queryEventsByEndAtBeforeAndStatusOpen(time: LocalDateTime): List<Event>\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/repository/EventCustomRepositoryImpl.kt",
    "content": "package band.gosrock.domain.domains.event.repository\n\nimport band.gosrock.domain.common.util.SliceUtil\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.event.domain.EventStatus\nimport band.gosrock.domain.domains.event.domain.EventStatus.CLOSED\nimport band.gosrock.domain.domains.event.domain.EventStatus.OPEN\nimport band.gosrock.domain.domains.event.domain.QEvent.event\nimport com.querydsl.core.types.OrderSpecifier\nimport com.querydsl.core.types.dsl.BooleanExpression\nimport com.querydsl.core.types.dsl.DateTemplate\nimport com.querydsl.core.types.dsl.Expressions\nimport com.querydsl.jpa.impl.JPAQueryFactory\nimport java.time.LocalDateTime\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.domain.Slice\n\nclass EventCustomRepositoryImpl(\n    private val queryFactory: JPAQueryFactory\n) : EventCustomRepository {\n\n    override fun querySliceEventsByHostIdIn(hostIds: List<Long>, pageable: Pageable): Slice<Event> {\n        val events = queryFactory\n            .selectFrom(event)\n            .where(hostIdIn(hostIds))\n            .orderBy(statusDesc(), createdAtDesc())\n            .offset(pageable.offset)\n            .limit(pageable.pageSize + 1L)\n            .fetch()\n        return SliceUtil.valueOf(events, pageable)\n    }\n\n    override fun querySliceEventsByStatus(status: EventStatus, pageable: Pageable): Slice<Event> {\n        val events = queryFactory\n            .selectFrom(event)\n            .where(statusEq(status))\n            .orderBy(statusDesc(), startAtAsc())\n            .offset(pageable.offset)\n            .limit(pageable.pageSize + 1L)\n            .fetch()\n        return SliceUtil.valueOf(events, pageable)\n    }\n\n    override fun querySliceEventsByKeyword(keyword: String?, pageable: Pageable): Slice<Event> {\n        val openEvents = queryFactory\n            .selectFrom(event)\n            .where(eqStatusOpen().and(nameContains(keyword)))\n            .orderBy(createdAtAsc())\n            .offset(pageable.offset)\n            .limit(pageable.pageSize + 1L)\n            .fetch()\n\n        val remainingSize = pageable.pageSize - openEvents.size\n        if (remainingSize >= 0) {\n            openEvents.addAll(queryClosedEventsByKeywordAndSize(keyword, pageable, remainingSize.toLong()))\n        }\n        return SliceUtil.valueOf(openEvents, pageable)\n    }\n\n    override fun queryEventsByEndAtBeforeAndStatusOpen(time: LocalDateTime): List<Event> =\n        queryFactory.selectFrom(event).where(endAtBefore(time), statusEq(OPEN)).fetch()\n\n    private fun queryClosedEventsByKeywordAndSize(keyword: String?, pageable: Pageable, size: Long): List<Event> {\n        val totalOpenEventsSize = queryCountByKeywordAndStatus(keyword, OPEN)\n        val closedEventsOffset = maxOf(pageable.offset - totalOpenEventsSize, 0L)\n        return queryFactory\n            .selectFrom(event)\n            .where(eqStatusClosed().and(nameContains(keyword)))\n            .orderBy(startAtDesc())\n            .offset(closedEventsOffset)\n            .limit(size + 1)\n            .fetch()\n    }\n\n    private fun queryCountByKeywordAndStatus(keyword: String?, status: EventStatus): Long =\n        queryFactory\n            .from(event)\n            .where(statusEq(status).and(nameContains(keyword)))\n            .fetchCount()\n\n    private fun hostIdIn(hostIds: List<Long>): BooleanExpression =\n        event.hostId.`in`(hostIds)\n\n    private fun eqStatusOpen(): BooleanExpression = event.status.eq(OPEN)\n\n    private fun eqStatusClosed(): BooleanExpression = event.status.eq(CLOSED)\n\n    private fun statusEq(status: EventStatus): BooleanExpression = event.status.eq(status)\n\n    private fun nameContains(keyword: String?): BooleanExpression? =\n        if (keyword == null) null else event.eventBasic.name.containsIgnoreCase(keyword)\n\n    private fun createdAtDesc(): OrderSpecifier<LocalDateTime> = event.createdAt.desc()\n\n    private fun createdAtAsc(): OrderSpecifier<LocalDateTime> = event.createdAt.asc()\n\n    private fun startAtAsc(): OrderSpecifier<LocalDateTime> = event.eventBasic.startAt.asc()\n\n    private fun startAtDesc(): OrderSpecifier<LocalDateTime> = event.eventBasic.startAt.desc()\n\n    private fun statusDesc(): OrderSpecifier<EventStatus> = event.status.desc()\n\n    private fun endAtBefore(time: LocalDateTime): BooleanExpression {\n        val eventEndAtTemplate: DateTemplate<LocalDateTime> = Expressions.dateTemplate(\n            LocalDateTime::class.java,\n            \"TIMESTAMPADD(MINUTE,{0}, {1}) \",\n            event.eventBasic.runTime,\n            event.eventBasic.startAt\n        )\n        return eventEndAtTemplate.before(time)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/repository/EventDetailImageRepository.kt",
    "content": "package band.gosrock.domain.domains.event.repository\n\nimport band.gosrock.domain.domains.event.domain.EventDetailImage\nimport org.springframework.data.repository.CrudRepository\n\ninterface EventDetailImageRepository : CrudRepository<EventDetailImage, Long>\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/repository/EventRepository.kt",
    "content": "package band.gosrock.domain.domains.event.repository\n\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.event.domain.EventStatus\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.jpa.repository.Query\nimport org.springframework.data.repository.CrudRepository\nimport org.springframework.data.repository.query.Param\n\ninterface EventRepository : CrudRepository<Event, Long>, EventCustomRepository {\n    override fun findAll(): List<Event>\n\n    fun findAllByHostId(hostId: Long, pageable: Pageable): Page<Event>\n\n    fun findAllByIdIn(ids: List<Long>): List<Event>\n\n    fun findAllByHostIdIn(hostIds: List<Long>, pageable: Pageable): Page<Event>\n\n    /** Admin: 키워드(이벤트명/호스트명) + 상태 필터로 이벤트 검색 (@Where 바이패스를 위해 native query 사용) */\n    @Query(\n        value = \"SELECT e.* FROM tbl_event e \" +\n            \"LEFT JOIN tbl_host h ON h.host_id = e.host_id \" +\n            \"WHERE (:keyword IS NULL OR e.name LIKE CONCAT('%', :keyword, '%') \" +\n            \"OR h.name LIKE CONCAT('%', :keyword, '%')) \" +\n            \"AND (:status IS NULL OR e.status = :status)\",\n        countQuery = \"SELECT COUNT(*) FROM tbl_event e \" +\n            \"LEFT JOIN tbl_host h ON h.host_id = e.host_id \" +\n            \"WHERE (:keyword IS NULL OR e.name LIKE CONCAT('%', :keyword, '%') \" +\n            \"OR h.name LIKE CONCAT('%', :keyword, '%')) \" +\n            \"AND (:status IS NULL OR e.status = :status)\",\n        nativeQuery = true\n    )\n    fun findAllForAdmin(\n        @Param(\"keyword\") keyword: String?,\n        @Param(\"status\") status: String?,\n        pageable: Pageable\n    ): Page<Event>\n\n    /** Admin: ID로 이벤트 조회 (@Where 바이패스) */\n    @Query(\n        value = \"SELECT * FROM tbl_event e WHERE e.event_id = :eventId\",\n        nativeQuery = true\n    )\n    fun findByIdForAdmin(@Param(\"eventId\") eventId: Long): Event?\n\n    /** Admin: 상태별 이벤트 수 (DELETED 포함을 위해 native query) */\n    @Query(\n        value = \"SELECT COUNT(*) FROM tbl_event e WHERE e.status = :status\",\n        nativeQuery = true\n    )\n    fun countByStatusNative(@Param(\"status\") status: String): Long\n\n    /** Admin: 최근 이벤트 N건 (DELETED 포함을 위해 native query) */\n    @Query(\n        value = \"SELECT * FROM tbl_event e ORDER BY e.created_at DESC LIMIT :limit\",\n        nativeQuery = true\n    )\n    fun findTopNByOrderByCreatedAtDesc(@Param(\"limit\") limit: Int): List<Event>\n\n    /** Admin: 페이지네이션 없이 전체 이벤트 조회 (엑셀 다운로드용) */\n    @Query(\n        value = \"SELECT e.* FROM tbl_event e \" +\n            \"LEFT JOIN tbl_host h ON h.host_id = e.host_id \" +\n            \"WHERE (:keyword IS NULL OR e.name LIKE CONCAT('%', :keyword, '%') \" +\n            \"OR h.name LIKE CONCAT('%', :keyword, '%')) \" +\n            \"AND (:status IS NULL OR e.status = :status)\",\n        nativeQuery = true\n    )\n    fun findAllForAdminNoPage(\n        @Param(\"keyword\") keyword: String?,\n        @Param(\"status\") status: String?,\n    ): List<Event>\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/event/service/EventService.kt",
    "content": "package band.gosrock.domain.domains.event.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.event.domain.EventBasic\nimport band.gosrock.domain.domains.event.domain.EventDetail\nimport band.gosrock.domain.domains.event.domain.EventPlace\nimport band.gosrock.domain.domains.event.domain.EventStatus\nimport band.gosrock.domain.domains.event.exception.CannotDeleteByIssuedTicketException\nimport band.gosrock.domain.domains.event.exception.CannotOpenEventException\nimport band.gosrock.domain.domains.event.exception.UseOtherApiException\nimport band.gosrock.domain.domains.event.repository.EventRepository\nimport band.gosrock.domain.domains.issuedTicket.adaptor.IssuedTicketAdaptor\nimport band.gosrock.domain.domains.ticket_item.service.TicketItemService\nimport java.time.LocalDateTime\nimport org.springframework.transaction.annotation.Transactional\n\n@DomainService\n@Transactional(readOnly = true)\nclass EventService(\n    private val eventRepository: EventRepository,\n    private val eventAdaptor: EventAdaptor,\n    private val ticketItemService: TicketItemService,\n    private val issuedTicketAdaptor: IssuedTicketAdaptor,\n) {\n\n    fun createEvent(event: Event): Event = eventRepository.save(event)\n\n    fun updateEventBasic(event: Event, eventBasic: EventBasic, eventPlace: EventPlace): Event {\n        event.updateEventBasic(eventBasic)\n        event.updateEventPlace(eventPlace)\n        return eventRepository.save(event)\n    }\n\n    fun updateEventDetail(event: Event, eventDetail: EventDetail): Event {\n        event.updateEventDetail(eventDetail)\n        return eventRepository.save(event)\n    }\n\n    fun updateEventPlace(event: Event, eventPlace: EventPlace): Event {\n        event.updateEventPlace(eventPlace)\n        return eventRepository.save(event)\n    }\n\n    fun validateEventBasicExistence(event: Event) {\n        if (!event.hasEventBasic() || !event.hasEventPlace()) throw CannotOpenEventException.EXCEPTION\n    }\n\n    fun validateEventDetailExistence(event: Event) {\n        if (!event.hasEventDetail()) throw CannotOpenEventException.EXCEPTION\n    }\n\n    fun openEvent(event: Event): Event {\n        validateEventBasicExistence(event)\n        validateEventDetailExistence(event)\n        ticketItemService.validateExistenceByEventId(event.id!!)\n        event.open()\n        return eventRepository.save(event)\n    }\n\n    fun updateEventStatus(event: Event, status: EventStatus): Event {\n        when (status) {\n            EventStatus.CLOSED -> event.close()\n            EventStatus.CALCULATING -> event.calculate()\n            EventStatus.PREPARING -> event.prepare()\n            else -> throw UseOtherApiException.EXCEPTION // open, deleteSoft 는 다른 API 강제\n        }\n        return eventRepository.save(event)\n    }\n\n    fun closeExpiredEventsEndAtBefore(time: LocalDateTime): List<Event> {\n        val events = eventAdaptor.queryEventsByEndAtBeforeAndStatusOpen(time)\n        events.forEach { event ->\n            updateEventStatus(event, EventStatus.CALCULATING)\n            updateEventStatus(event, EventStatus.CLOSED)\n        }\n        eventRepository.saveAll(events)\n        return events\n    }\n\n    fun deleteEventSoft(event: Event): Event {\n        // 발급된 티켓이 있다면 삭제 불가\n        if (issuedTicketAdaptor.existsByEventId(event.id!!)) throw CannotDeleteByIssuedTicketException.EXCEPTION\n        event.deleteSoft()\n        return eventRepository.save(event)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/example/domain/ExampleEntity.kt",
    "content": "package band.gosrock.domain.domains.example.domain\n\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport jakarta.persistence.Entity\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\nimport jakarta.persistence.Table\n\n@Table(name = \"tbl_example\")\n@Entity\nclass ExampleEntity(\n    var content: String? = null,\n) : BaseTimeEntity() {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    val id: Long? = null\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/example/repository/ExampleRepository.kt",
    "content": "package band.gosrock.domain.domains.example.repository\n\nimport band.gosrock.domain.domains.example.domain.ExampleEntity\nimport org.springframework.data.jpa.repository.JpaRepository\n\ninterface ExampleRepository : JpaRepository<ExampleEntity, Long>\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/example/service/ExampleDomainService.kt",
    "content": "package band.gosrock.domain.domains.example.service\n\nimport band.gosrock.common.exception.DuDoongDynamicException\nimport band.gosrock.domain.domains.example.domain.ExampleEntity\nimport band.gosrock.domain.domains.example.repository.ExampleRepository\nimport org.springframework.stereotype.Service\n\n@Service\nclass ExampleDomainService(\n    private val exampleRepository: ExampleRepository\n) {\n\n    fun exception() {\n        throw DuDoongDynamicException(400, \"에러코드\", \"메세지\")\n    }\n\n    fun query(id: Long): ExampleEntity {\n        return exampleRepository.findById(id)\n            .orElseThrow { DuDoongDynamicException(400, \"에러코드\", \"메세지\") }\n    }\n\n    fun save(content: String): ExampleEntity {\n        val entity = ExampleEntity(content)\n        return exampleRepository.save(entity)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/adaptor/HostAdaptor.kt",
    "content": "package band.gosrock.domain.domains.host.adaptor\n\nimport band.gosrock.common.annotation.Adaptor\nimport band.gosrock.domain.domains.host.domain.Host\nimport band.gosrock.domain.domains.host.exception.HostNotFoundException\nimport band.gosrock.domain.domains.host.repository.HostRepository\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.domain.Slice\n\n@Adaptor\nclass HostAdaptor(private val hostRepository: HostRepository) {\n\n    fun findById(hostId: Long): Host =\n        hostRepository.findById(hostId).orElseThrow { HostNotFoundException.EXCEPTION }\n\n    /** 자신이 속해있는 호스트 리스트를 무한스크롤로 가져오는 쿼리 요청 */\n    fun querySliceHostsByUserId(userId: Long, pageable: Pageable): Slice<Host> =\n        hostRepository.querySliceHostsByUserId(userId, pageable)\n\n    /** 자신이 마스터인 호스트 리스트를 가져오는 쿼리 요청 */\n    fun findAllByMasterUserId(userId: Long): List<Host> =\n        hostRepository.findAllByMasterUserId(userId)\n\n    /** 자신이 속해있는 호스트 리스트를 가져오는 쿼리 요청 */\n    fun findAllByHostUsers_UserId(userId: Long, pageable: Pageable): Page<Host> =\n        hostRepository.findAllByHostUsers_UserId(userId, pageable)\n\n    fun findAllByHostUsers_UserId(userId: Long): List<Host> =\n        hostRepository.findAllByHostUsers_UserId(userId)\n\n    fun findAllForAdmin(keyword: String?, pageable: Pageable): Page<Host> =\n        hostRepository.findAllForAdmin(keyword, pageable)\n\n    /** 자신이 속해있는 호스트 리스트 중 초대 수락한 호스트만 가져오는 쿼리 요청 */\n    fun querySliceHostsByActiveUserId(userId: Long): List<Host> =\n        hostRepository.queryHostsByActiveUserId(userId)\n\n    fun querySliceHostsByActiveUserId(userId: Long, pageable: Pageable): Slice<Host> =\n        hostRepository.querySliceHostsByActiveUserId(userId, pageable)\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/domain/Host.kt",
    "content": "package band.gosrock.domain.domains.host.domain\n\nimport band.gosrock.domain.common.aop.domainEvent.Events\nimport band.gosrock.domain.common.events.host.HostRegisterSlackEvent\nimport band.gosrock.domain.common.events.host.HostUserInvitationEvent\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport band.gosrock.domain.common.vo.HostInfoVo\nimport band.gosrock.domain.common.vo.HostProfileVo\nimport band.gosrock.domain.domains.host.exception.AlreadyJoinedHostException\nimport band.gosrock.domain.domains.host.exception.CannotModifyMasterHostRoleException\nimport band.gosrock.domain.domains.host.exception.ForbiddenHostException\nimport band.gosrock.domain.domains.host.exception.HostUserNotFoundException\nimport band.gosrock.domain.domains.host.exception.NotAcceptedHostException\nimport band.gosrock.domain.domains.host.exception.NotManagerHostException\nimport band.gosrock.domain.domains.host.exception.NotMasterHostException\nimport band.gosrock.domain.domains.host.exception.NotPartnerHostException\nimport band.gosrock.domain.domains.host.exception.DuplicateSlackUrlException\nimport org.apache.commons.codec.binary.StringUtils\nimport jakarta.persistence.CascadeType\nimport jakarta.persistence.Column\nimport jakarta.persistence.Embedded\nimport jakarta.persistence.Entity\nimport jakarta.persistence.FetchType\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\nimport jakarta.persistence.OneToMany\nimport jakarta.persistence.OrderBy\n\n@Entity(name = \"tbl_host\")\nclass Host(\n    // 마스터 유저 id\n    var masterUserId: Long? = null,\n    // 슬랙 웹훅 url\n    var slackUrl: String? = null,\n    name: String? = null,\n    introduce: String? = null,\n    profileImageKey: String? = null,\n    contactEmail: String? = null,\n    contactNumber: String? = null,\n) : BaseTimeEntity() {\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"host_id\")\n    var id: Long? = null\n        protected set\n\n    @Embedded\n    var profile: HostProfile? = HostProfile(\n        name = name,\n        introduce = introduce,\n        profileImageKey = profileImageKey,\n        contactEmail = contactEmail,\n        contactNumber = contactNumber,\n    )\n        protected set\n\n    // 파트너 여부\n    var partner: Boolean = false\n        protected set\n\n    // 단방향 oneToMany 매핑\n    @OneToMany(\n        mappedBy = \"host\",\n        cascade = [CascadeType.ALL],\n        orphanRemoval = true,\n        fetch = FetchType.EAGER,\n    )\n    @OrderBy(\"createdAt DESC\")\n    val hostUsers: MutableSet<HostUser> = HashSet()\n\n    fun addHostUsers(hostUserList: Set<HostUser>) {\n        hostUserList.forEach { validateHostUserExistence(it) }\n        this.hostUsers.addAll(hostUserList)\n    }\n\n    fun inviteHostUsers(hostUserList: Set<HostUser>) {\n        hostUserList.forEach { validateHostUserExistence(it) }\n        this.hostUsers.addAll(hostUserList)\n        hostUserList.forEach { Events.raise(HostUserInvitationEvent.of(this, it)) }\n    }\n\n    fun hasHostUserId(userId: Long): Boolean =\n        this.hostUsers.any { it.userId == userId }\n\n    fun hasHostUser(hostUser: HostUser): Boolean = hasHostUserId(hostUser.userId!!)\n\n    fun getHostUserByUserId(userId: Long): HostUser =\n        this.hostUsers.firstOrNull { it.userId == userId }\n            ?: throw HostUserNotFoundException.EXCEPTION\n\n    fun getHostUser_UserIds(): List<Long> =\n        this.hostUsers.mapNotNull { it.userId }\n\n    fun updateProfile(hostProfile: HostProfile) {\n        this.profile?.updateProfile(hostProfile)\n    }\n\n    fun updateSlackUrl(slackUrl: String) {\n        if (StringUtils.equals(this.slackUrl, slackUrl)) throw DuplicateSlackUrlException.EXCEPTION\n        Events.raise(HostRegisterSlackEvent.of(this))\n        this.slackUrl = slackUrl\n    }\n\n    fun isManagerHostUserId(userId: Long): Boolean =\n        this.hostUsers.any { it.userId == userId && it.role == HostRole.MANAGER }\n\n    fun isActiveHostUserId(userId: Long): Boolean =\n        this.hostUsers.any { it.userId == userId && it.active }\n\n    fun setHostUserRole(userId: Long, role: HostRole) {\n        if (this.masterUserId == userId) throw CannotModifyMasterHostRoleException.EXCEPTION\n        this.hostUsers.firstOrNull { it.userId == userId }\n            ?.setHostRole(role)\n            ?: throw HostUserNotFoundException.EXCEPTION\n    }\n\n    fun removeHostUser(userId: Long) {\n        if (this.isActiveHostUserId(userId)) throw AlreadyJoinedHostException.EXCEPTION\n        this.hostUsers.remove(this.getHostUserByUserId(userId))\n    }\n\n    /** 해당 유저가 호스트에 이미 속하는지 확인하는 검증 로직입니다 */\n    fun validateHostUserIdExistence(userId: Long) {\n        if (this.hasHostUserId(userId)) throw AlreadyJoinedHostException.EXCEPTION\n    }\n\n    fun validateHostUserExistence(hostUser: HostUser) {\n        validateHostUserIdExistence(hostUser.userId!!)\n    }\n\n    /** 해당 유저가 호스트에 속하는지 확인하는 검증 로직입니다 */\n    fun validateHostUser(userId: Long) {\n        if (!this.hasHostUserId(userId)) throw ForbiddenHostException.EXCEPTION\n    }\n\n    /** 해당 유저가 호스트에 속하며 가입 승인을 완료했는지 (활성상태) 확인하는 검증 로직입니다 */\n    fun validateActiveHostUser(userId: Long) {\n        this.validateHostUser(userId)\n        if (!this.isActiveHostUserId(userId)) throw NotAcceptedHostException.EXCEPTION\n    }\n\n    /** 해당 유저가 매니저 이상인지 확인하는 검증 로직입니다 */\n    fun validateManagerHostUser(userId: Long) {\n        this.validateActiveHostUser(userId)\n        if (!this.isManagerHostUserId(userId) && this.masterUserId != userId) {\n            throw NotManagerHostException.EXCEPTION\n        }\n    }\n\n    /** 해당 유저가 호스트의 마스터(담당자, 방장)인지 확인하는 검증 로직입니다 */\n    fun validateMasterHostUser(userId: Long) {\n        this.validateActiveHostUser(userId)\n        if (this.masterUserId != userId) throw NotMasterHostException.EXCEPTION\n    }\n\n    /** 해당 호스트가 파트너 인지 검증합니다. */\n    fun validatePartnerHost() {\n        if (!partner) throw NotPartnerHostException.EXCEPTION\n    }\n\n    /** 마스터 권한을 다른 활성 멤버에게 양도합니다. 현재 마스터만 호출 가능합니다. */\n    fun transferMaster(currentMasterUserId: Long, newMasterUserId: Long) {\n        validateMasterHostUser(currentMasterUserId)\n        validateActiveHostUser(newMasterUserId)\n        // 기존 마스터 → MANAGER\n        this.hostUsers.first { it.userId == currentMasterUserId }.setHostRole(HostRole.MANAGER)\n        // 새 마스터 → MASTER\n        this.hostUsers.first { it.userId == newMasterUserId }.setHostRole(HostRole.MASTER)\n        this.masterUserId = newMasterUserId\n    }\n\n    /** 어드민이 마스터 권한을 강제 양도합니다. 권한 검증 없이 실행됩니다. */\n    fun forceTransferMaster(newMasterUserId: Long) {\n        validateActiveHostUser(newMasterUserId)\n        // 기존 마스터 → MANAGER (있으면)\n        this.hostUsers.firstOrNull { it.userId == masterUserId }?.setHostRole(HostRole.MANAGER)\n        // 새 마스터 → MASTER\n        this.hostUsers.first { it.userId == newMasterUserId }.setHostRole(HostRole.MASTER)\n        this.masterUserId = newMasterUserId\n    }\n\n    fun isPartnerHost(): Boolean = partner\n\n    fun changePartner(partner: Boolean) {\n        this.partner = partner\n    }\n\n    fun toHostInfoVo(): HostInfoVo = HostInfoVo.from(this)\n    fun toHostProfileVo(): HostProfileVo = HostProfileVo.from(this)\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/domain/HostProfile.kt",
    "content": "package band.gosrock.domain.domains.host.domain\n\nimport band.gosrock.domain.common.vo.ImageVo\nimport jakarta.persistence.Column\nimport jakarta.persistence.Embeddable\nimport jakarta.persistence.Embedded\n\n@Embeddable\nclass HostProfile(\n    // 호스트 이름\n    @Column(length = 15)\n    var name: String? = null,\n    // 간단 소개\n    var introduce: String? = null,\n    profileImageKey: String? = null,\n    // 대표자 이메일\n    var contactEmail: String? = null,\n    // 대표자 연락처\n    @Column(length = 15)\n    var contactNumber: String? = null,\n) {\n    // 프로필 이미지 url\n    @Embedded\n    var profileImage: ImageVo? = ImageVo.valueOf(profileImageKey)\n\n    fun updateProfile(hostProfile: HostProfile) {\n        this.name = hostProfile.name\n        this.profileImage = hostProfile.profileImage\n        this.introduce = hostProfile.introduce\n        this.contactEmail = hostProfile.contactEmail\n        this.contactNumber = hostProfile.contactNumber\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/domain/HostRole.kt",
    "content": "package band.gosrock.domain.domains.host.domain\n\nimport com.fasterxml.jackson.annotation.JsonCreator\nimport com.fasterxml.jackson.annotation.JsonValue\n\nenum class HostRole(val name2: String, @JsonValue val value: String) {\n    // 마스터 (모든 권한)\n    MASTER(\"MASTER\", \"마스터\"),\n    // 슈퍼 호스트 (조회, 변경 가능)\n    MANAGER(\"MANAGER\", \"매니저\"),\n    // 일반 호스트 (조회만 가능)\n    GUEST(\"GUEST\", \"게스트\");\n\n    companion object {\n        // Enum Validation 을 위한 코드, enum 에 속하지 않으면 null 리턴\n        @JvmStatic\n        @JsonCreator\n        fun fromHostRole(val2: String): HostRole? =\n            values().firstOrNull { it.name == val2 }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/domain/HostUser.kt",
    "content": "package band.gosrock.domain.domains.host.domain\n\nimport band.gosrock.domain.common.aop.domainEvent.Events\nimport band.gosrock.domain.common.events.host.HostUserJoinEvent\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport band.gosrock.domain.domains.host.exception.AlreadyJoinedHostException\nimport jakarta.persistence.Column\nimport jakarta.persistence.Entity\nimport jakarta.persistence.EnumType\nimport jakarta.persistence.Enumerated\nimport jakarta.persistence.FetchType\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\nimport jakarta.persistence.JoinColumn\nimport jakarta.persistence.ManyToOne\nimport jakarta.persistence.Table\nimport jakarta.persistence.UniqueConstraint\n\n@Entity(name = \"tbl_host_user\")\n@Table(uniqueConstraints = [UniqueConstraint(columnNames = [\"host_id\", \"user_id\"])])\nclass HostUser(\n    // 소속 호스트를 관리중인 유저 아이디\n    @Column(name = \"user_id\")\n    var userId: Long? = null,\n\n    // 유저의 권한\n    @Enumerated(EnumType.STRING)\n    var role: HostRole = HostRole.GUEST,\n) : BaseTimeEntity() {\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"host_user_id\")\n    var id: Long? = null\n        protected set\n\n    // 소속 호스트 아이디\n    @ManyToOne(fetch = FetchType.EAGER)\n    @JoinColumn(name = \"host_id\")\n    var host: Host? = null\n        protected set\n\n    // 초대 승락 여부\n    var active: Boolean = false\n        protected set\n\n    constructor(host: Host, userId: Long?, role: HostRole) : this(userId = userId, role = role) {\n        this.host = host\n    }\n\n    fun setHostRole(role: HostRole) {\n        this.role = role\n    }\n\n    fun activate() {\n        if (this.active) throw AlreadyJoinedHostException.EXCEPTION\n        this.active = true\n        Events.raise(HostUserJoinEvent.of(this.host!!.id, this.userId))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/exception/AlreadyJoinedHostException.kt",
    "content": "package band.gosrock.domain.domains.host.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass AlreadyJoinedHostException private constructor() : DuDoongCodeException(HostErrorCode.ALREADY_JOINED_HOST) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = AlreadyJoinedHostException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/exception/CannotModifyMasterHostRoleException.kt",
    "content": "package band.gosrock.domain.domains.host.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CannotModifyMasterHostRoleException private constructor() : DuDoongCodeException(HostErrorCode.CANNOT_MODIFY_MASTER_HOST_ROLE) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CannotModifyMasterHostRoleException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/exception/DuplicateSlackUrlException.kt",
    "content": "package band.gosrock.domain.domains.host.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass DuplicateSlackUrlException private constructor() : DuDoongCodeException(HostErrorCode.DUPLICATED_SLACK_URL) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = DuplicateSlackUrlException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/exception/ForbiddenHostException.kt",
    "content": "package band.gosrock.domain.domains.host.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass ForbiddenHostException private constructor() : DuDoongCodeException(HostErrorCode.FORBIDDEN_HOST) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = ForbiddenHostException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/exception/HostErrorCode.kt",
    "content": "package band.gosrock.domain.domains.host.exception\n\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.consts.DuDoongStatic.BAD_REQUEST\nimport band.gosrock.common.consts.DuDoongStatic.NOT_FOUND\nimport band.gosrock.common.dto.ErrorReason\nimport band.gosrock.common.exception.BaseErrorCode\nimport java.lang.reflect.Field\n\nenum class HostErrorCode(\n    private val status: Int,\n    private val code: String,\n    private val reason: String,\n) : BaseErrorCode {\n    NOT_MANAGER_HOST(BAD_REQUEST, \"HOST_400_1\", \"매니저 권한이 없는 유저입니다.\"),\n    FORBIDDEN_HOST(BAD_REQUEST, \"HOST_400_2\", \"해당 호스트에 대한 접근 권한이 없습니다.\"),\n    ALREADY_JOINED_HOST(BAD_REQUEST, \"HOST_400_3\", \"이미 가입되어 있는 유저입니다.\"),\n    NOT_MASTER_HOST(BAD_REQUEST, \"HOST_400_4\", \"마스터 호스트 권한이 없는 유저입니다.\"),\n    CANNOT_MODIFY_MASTER_HOST_ROLE(BAD_REQUEST, \"HOST_400_5\", \"마스터 호스트의 권한은 변경할 수 없습니다.\"),\n    NOT_ACCEPTED_HOST(BAD_REQUEST, \"HOST_400_6\", \"아직 초대를 수락하지 않은 유저입니다.\"),\n    NOT_PARTNER_HOST(BAD_REQUEST, \"HOST_400_7\", \"파트너 호스트만 사용할 수 있는 기능입니다. 제휴 신청을 해주세요.\"),\n    DUPLICATED_SLACK_URL(BAD_REQUEST, \"HOST_400_8\", \"기존과 동일한 슬랙 url 입니다.\"),\n    INVALID_SLACK_URL(BAD_REQUEST, \"HOST_400_9\", \"유효하지 않은 않은 슬랙 url 입니다.\"),\n    HOST_NOT_FOUND(NOT_FOUND, \"Host_404_1\", \"해당 호스트를 찾을 수 없습니다.\"),\n    HOST_USER_NOT_FOUND(NOT_FOUND, \"HOST_404_2\", \"가입된 호스트 유저가 아닙니다.\");\n\n    override fun getErrorReason(): ErrorReason =\n        ErrorReason(status = status, code = code, reason = reason)\n\n    override fun getExplainError(): String {\n        val field: Field = this.javaClass.getField(this.name)\n        val annotation = field.getAnnotation(ExplainError::class.java)\n        return annotation?.value ?: reason\n    }\n\n    fun getReason(): String = reason\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/exception/HostNotFoundException.kt",
    "content": "package band.gosrock.domain.domains.host.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass HostNotFoundException private constructor() : DuDoongCodeException(HostErrorCode.HOST_NOT_FOUND) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = HostNotFoundException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/exception/HostUserNotFoundException.kt",
    "content": "package band.gosrock.domain.domains.host.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass HostUserNotFoundException private constructor() : DuDoongCodeException(HostErrorCode.HOST_USER_NOT_FOUND) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = HostUserNotFoundException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/exception/InvalidSlackUrlException.kt",
    "content": "package band.gosrock.domain.domains.host.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass InvalidSlackUrlException private constructor() : DuDoongCodeException(HostErrorCode.INVALID_SLACK_URL) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = InvalidSlackUrlException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/exception/NotAcceptedHostException.kt",
    "content": "package band.gosrock.domain.domains.host.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass NotAcceptedHostException private constructor() : DuDoongCodeException(HostErrorCode.NOT_ACCEPTED_HOST) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = NotAcceptedHostException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/exception/NotManagerHostException.kt",
    "content": "package band.gosrock.domain.domains.host.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass NotManagerHostException private constructor() : DuDoongCodeException(HostErrorCode.NOT_MANAGER_HOST) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = NotManagerHostException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/exception/NotMasterHostException.kt",
    "content": "package band.gosrock.domain.domains.host.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass NotMasterHostException private constructor() : DuDoongCodeException(HostErrorCode.NOT_MASTER_HOST) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = NotMasterHostException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/exception/NotPartnerHostException.kt",
    "content": "package band.gosrock.domain.domains.host.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass NotPartnerHostException private constructor() : DuDoongCodeException(HostErrorCode.NOT_PARTNER_HOST) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = NotPartnerHostException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/repository/HostCustomRepository.kt",
    "content": "package band.gosrock.domain.domains.host.repository\n\nimport band.gosrock.domain.domains.host.domain.Host\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.domain.Slice\n\ninterface HostCustomRepository {\n    fun querySliceHostsByUserId(id: Long, pageable: Pageable): Slice<Host>\n    fun queryHostsByActiveUserId(id: Long): List<Host>\n    fun querySliceHostsByActiveUserId(id: Long, pageable: Pageable): Slice<Host>\n    fun findAllForAdmin(keyword: String?, pageable: Pageable): Page<Host>\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/repository/HostCustomRepositoryImpl.kt",
    "content": "package band.gosrock.domain.domains.host.repository\n\nimport band.gosrock.domain.common.util.SliceUtil\nimport band.gosrock.domain.domains.host.domain.Host\nimport band.gosrock.domain.domains.host.domain.QHost.host\nimport band.gosrock.domain.domains.host.domain.QHostUser.hostUser\nimport com.querydsl.core.types.OrderSpecifier\nimport com.querydsl.core.types.dsl.BooleanExpression\nimport com.querydsl.jpa.impl.JPAQueryFactory\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.PageImpl\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.domain.Slice\n\nclass HostCustomRepositoryImpl(private val queryFactory: JPAQueryFactory) : HostCustomRepository {\n\n    override fun querySliceHostsByUserId(userId: Long, pageable: Pageable): Slice<Host> {\n        val hosts = queryFactory\n            .select(host)\n            .from(host, hostUser)\n            .where(hostUserIdEq(userId), host.hostUsers.contains(hostUser))\n            .offset(pageable.offset)\n            .orderBy(hostIdDesc())\n            .limit((pageable.pageSize + 1).toLong())\n            .fetch()\n        return SliceUtil.valueOf(hosts, pageable)\n    }\n\n    override fun queryHostsByActiveUserId(userId: Long): List<Host> =\n        queryFactory\n            .select(host)\n            .from(host, hostUser)\n            .where(hostUserIdEq(userId), host.hostUsers.contains(hostUser), hostUserActive())\n            .fetch()\n\n    override fun querySliceHostsByActiveUserId(userId: Long, pageable: Pageable): Slice<Host> {\n        val hosts = queryFactory\n            .select(host)\n            .from(host, hostUser)\n            .where(hostUserIdEq(userId), host.hostUsers.contains(hostUser), hostUserActive())\n            .offset(pageable.offset)\n            .orderBy(hostIdDesc())\n            .limit((pageable.pageSize + 1).toLong())\n            .fetch()\n        return SliceUtil.valueOf(hosts, pageable)\n    }\n\n    override fun findAllForAdmin(keyword: String?, pageable: Pageable): Page<Host> {\n        val keywordCondition: BooleanExpression? =\n            if (!keyword.isNullOrBlank()) host.profile.name.contains(keyword) else null\n\n        val hosts = queryFactory\n            .select(host)\n            .from(host)\n            .where(keywordCondition)\n            .offset(pageable.offset)\n            .orderBy(hostIdDesc())\n            .limit(pageable.pageSize.toLong())\n            .fetch()\n\n        val total = queryFactory\n            .select(host.count())\n            .from(host)\n            .where(keywordCondition)\n            .fetchOne() ?: 0L\n\n        return PageImpl(hosts, pageable, total)\n    }\n\n    private fun hostUserIdEq(userId: Long): BooleanExpression = hostUser.userId.eq(userId)\n    private fun hostUserActive(): BooleanExpression = hostUser.active.isTrue\n    private fun hostIdDesc(): OrderSpecifier<Long> = host.id.desc()\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/repository/HostRepository.kt",
    "content": "package band.gosrock.domain.domains.host.repository\n\nimport band.gosrock.domain.domains.host.domain.Host\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.repository.CrudRepository\n\ninterface HostRepository : CrudRepository<Host, Long>, HostCustomRepository {\n    fun findAllByMasterUserId(userId: Long): List<Host>\n    fun findAllByHostUsers_UserId(userId: Long): List<Host>\n    fun findAllByHostUsers_UserId(userId: Long, pageable: Pageable): Page<Host>\n    fun findByHostUsersIdIn(userId: List<Long>): List<Host>\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/host/service/HostService.kt",
    "content": "package band.gosrock.domain.domains.host.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.common.aop.redissonLock.RedissonLock\nimport band.gosrock.domain.domains.host.adaptor.HostAdaptor\nimport band.gosrock.domain.domains.host.domain.Host\nimport band.gosrock.domain.domains.host.domain.HostProfile\nimport band.gosrock.domain.domains.host.domain.HostRole\nimport band.gosrock.domain.domains.host.domain.HostUser\nimport band.gosrock.domain.domains.host.exception.InvalidSlackUrlException\nimport band.gosrock.domain.domains.host.repository.HostRepository\nimport org.apache.commons.codec.binary.StringUtils\nimport org.springframework.transaction.annotation.Transactional\n\n@DomainService\n@Transactional(readOnly = true)\nopen class HostService(\n    private val hostRepository: HostRepository,\n    private val hostAdaptor: HostAdaptor,\n) {\n    open fun createHost(host: Host): Host = hostRepository.save(host)\n\n    open fun addHostUser(host: Host, hostUser: HostUser): Host {\n        host.addHostUsers(setOf(hostUser))\n        return hostRepository.save(host)\n    }\n\n    @RedissonLock(LockName = \"호스트유저초대\", identifier = \"id\", paramClassType = Host::class)\n    open fun inviteHostUser(host: Host, hostUser: HostUser): Host {\n        host.inviteHostUsers(setOf(hostUser))\n        return hostRepository.save(host)\n    }\n\n    open fun updateHostUserRole(host: Host, userId: Long, role: HostRole): Host {\n        host.setHostUserRole(userId, role)\n        return hostRepository.save(host)\n    }\n\n    open fun updateHostProfile(host: Host, profile: HostProfile): Host {\n        host.updateProfile(profile)\n        return hostRepository.save(host)\n    }\n\n    open fun updateHostSlackUrl(host: Host, url: String): Host {\n        host.updateSlackUrl(url)\n        return hostRepository.save(host)\n    }\n\n    open fun activateHostUser(host: Host, userId: Long): Host {\n        host.getHostUserByUserId(userId).activate()\n        return hostRepository.save(host)\n    }\n\n    open fun removeHostUser(host: Host, userId: Long): Host {\n        host.removeHostUser(userId)\n        return hostRepository.save(host)\n    }\n\n    fun validateDuplicatedSlackUrl(host: Host, url: String) {\n        if (StringUtils.equals(host.slackUrl, url)) throw InvalidSlackUrlException.EXCEPTION\n    }\n\n    /** 해당 유저가 호스트에 속하는지 확인하는 검증 로직입니다 */\n    fun validateHostUser(host: Host, userId: Long) = host.validateHostUser(userId)\n\n    /** 해당 유저가 호스트의 마스터(담당자, 방장)인지 확인하는 검증 로직입니다 */\n    fun validateMasterHostUser(host: Host, userId: Long) = host.validateMasterHostUser(userId)\n\n    /** 해당 유저가 슈퍼 호스트인지 확인하는 검증 로직입니다 */\n    fun validateManagerHostUser(hostId: Long, userId: Long) {\n        val host = hostAdaptor.findById(hostId)\n        host.validateManagerHostUser(userId)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/adaptor/IssuedTicketAdaptor.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.adaptor\n\nimport band.gosrock.common.annotation.Adaptor\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTickets\nimport band.gosrock.domain.domains.issuedTicket.exception.IssuedTicketNotFoundException\nimport band.gosrock.domain.domains.issuedTicket.exception.IssuedTicketUserNotMatchedException\nimport band.gosrock.domain.domains.issuedTicket.repository.IssuedTicketRepository\nimport band.gosrock.domain.domains.issuedTicket.repository.condition.FindEventIssuedTicketsCondition\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\n\n@Adaptor\nclass IssuedTicketAdaptor(\n    private val issuedTicketRepository: IssuedTicketRepository,\n) {\n    fun save(issuedTicket: IssuedTicket): IssuedTicket =\n        issuedTicketRepository.save(issuedTicket)\n\n    fun saveAll(issuedTickets: List<IssuedTicket>) {\n        issuedTicketRepository.saveAll(issuedTickets)\n    }\n\n    fun findForUser(currentUserId: Long, uuid: String): IssuedTicket {\n        val issuedTicket = issuedTicketRepository.findByUuid(uuid)\n            .orElseThrow { IssuedTicketNotFoundException.EXCEPTION }\n        if (issuedTicket.userInfo?.userId != currentUserId) {\n            throw IssuedTicketUserNotMatchedException.EXCEPTION\n        }\n        return issuedTicket\n    }\n\n    fun queryIssuedTicket(issuedTicketId: Long): IssuedTicket =\n        issuedTicketRepository.find(issuedTicketId)\n            .orElseThrow { IssuedTicketNotFoundException.EXCEPTION }\n\n    fun existsByEventId(eventId: Long): Boolean =\n        issuedTicketRepository.existsByEventId(eventId)\n\n    fun searchIssuedTicket(page: Pageable, condition: FindEventIssuedTicketsCondition): Page<IssuedTicket> =\n        issuedTicketRepository.searchToPage(condition, page)\n\n    fun cancel(issuedTicket: IssuedTicket) {\n        issuedTicket.cancel()\n    }\n\n    fun findAllByOrderUuid(orderUuid: String): List<IssuedTicket> =\n        issuedTicketRepository.findAllByOrderUuid(orderUuid)\n\n    fun findOrderLineIssuedTickets(orderLineId: Long): IssuedTickets =\n        IssuedTickets.from(issuedTicketRepository.findAllByOrderLineId(orderLineId))\n\n    fun findOrderIssuedTickets(orderUuid: String): IssuedTickets =\n        IssuedTickets.from(issuedTicketRepository.findAllByOrderUuid(orderUuid))\n\n    fun countPaidTicket(userId: Long, itemId: Long): Long =\n        issuedTicketRepository.countPaidTicket(userId, itemId)\n\n    fun countIssuedTicketByItemId(itemId: Long): Long =\n        issuedTicketRepository.countIssuedTicketByItemId(itemId)\n\n    fun queryByIssuedTicketNo(issuedTicketNo: String): IssuedTicket =\n        issuedTicketRepository.findByIssuedTicketNo(issuedTicketNo)\n            .orElseThrow { IssuedTicketNotFoundException.EXCEPTION }\n\n    fun queryByIssuedTicketUuid(uuid: String): IssuedTicket =\n        issuedTicketRepository.findByUuid(uuid)\n            .orElseThrow { IssuedTicketNotFoundException.EXCEPTION }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/adaptor/IssuedTicketOptionAnswerAdaptor.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.adaptor\n\nimport band.gosrock.common.annotation.Adaptor\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketOptionAnswer\nimport band.gosrock.domain.domains.issuedTicket.repository.IssuedTicketOptionAnswerRepository\n\n@Adaptor\nclass IssuedTicketOptionAnswerAdaptor(\n    private val issuedTicketOptionAnswerRepository: IssuedTicketOptionAnswerRepository,\n) {\n    fun saveAll(issuedTicketOptionAnswers: List<IssuedTicketOptionAnswer>) {\n        issuedTicketOptionAnswerRepository.saveAll(issuedTicketOptionAnswers)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/domain/IssuedTicket.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.domain\n\nimport band.gosrock.common.consts.DuDoongStatic.NO_START_NUMBER\nimport band.gosrock.domain.common.aop.domainEvent.Events\nimport band.gosrock.domain.common.events.issuedTicket.EntranceIssuedTicketEvent\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport band.gosrock.domain.common.vo.IssuedTicketInfoVo\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.issuedTicket.exception.CanNotCancelEntranceException\nimport band.gosrock.domain.domains.issuedTicket.exception.CanNotCancelException\nimport band.gosrock.domain.domains.issuedTicket.exception.CanNotEntranceException\nimport band.gosrock.domain.domains.issuedTicket.exception.IssuedTicketAlreadyEntranceException\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderLineItem\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport band.gosrock.domain.domains.user.domain.User\nimport band.gosrock.infrastructure.config.mail.dto.EmailIssuedTicketInfo\nimport java.time.LocalDateTime\nimport java.util.UUID\nimport jakarta.persistence.CascadeType\nimport jakarta.persistence.Column\nimport jakarta.persistence.Embedded\nimport jakarta.persistence.Entity\nimport jakarta.persistence.EnumType\nimport jakarta.persistence.Enumerated\nimport jakarta.persistence.FetchType\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\nimport jakarta.persistence.JoinColumn\nimport jakarta.persistence.OneToMany\nimport jakarta.persistence.PostPersist\nimport jakarta.persistence.PrePersist\n\n@Entity(name = \"tbl_issued_ticket\")\nclass IssuedTicket(\n    var eventId: Long? = null,\n\n    @Embedded\n    var userInfo: IssuedTicketUserInfoVo? = null,\n\n    var orderUuid: String? = null,\n\n    var orderLineId: Long? = null,\n\n    @Embedded\n    var itemInfo: IssuedTicketItemInfoVo? = null,\n\n    issuedTicketStatus: IssuedTicketStatus? = null,\n\n    initialOptionAnswers: List<IssuedTicketOptionAnswer> = emptyList(),\n) : BaseTimeEntity() {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"issued_ticket_id\")\n    var id: Long? = null\n        protected set\n\n    var issuedTicketNo: String? = null\n        protected set\n\n    var enteredAt: LocalDateTime? = null\n        protected set\n\n    @OneToMany(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])\n    @JoinColumn(name = \"issued_ticket_id\")\n    var issuedTicketOptionAnswers: MutableList<IssuedTicketOptionAnswer> = mutableListOf()\n        protected set\n\n    @Column(nullable = false)\n    var uuid: String? = null\n        protected set\n\n    @Enumerated(EnumType.STRING)\n    var issuedTicketStatus: IssuedTicketStatus = issuedTicketStatus ?: IssuedTicketStatus.ENTRANCE_INCOMPLETE\n        protected set\n\n    init {\n        if (initialOptionAnswers.isNotEmpty()) {\n            this.issuedTicketOptionAnswers.addAll(initialOptionAnswers)\n        }\n    }\n\n    fun addOptionAnswers(answers: List<IssuedTicketOptionAnswer>) {\n        issuedTicketOptionAnswers.addAll(answers)\n    }\n\n    /** ---------------------------- 생성 관련 메서드 ---------------------------------- */\n\n    @PrePersist\n    fun createUUID() {\n        this.uuid = UUID.randomUUID().toString()\n    }\n\n    @PostPersist\n    fun createIssuedTicketNo() {\n        this.issuedTicketNo = \"T\" + java.lang.Long.sum(NO_START_NUMBER, this.id!!)\n    }\n\n    fun sumOptionPrice(): Money =\n        issuedTicketOptionAnswers\n            .map { it.additionalPrice }\n            .fold(Money.ZERO) { acc, money -> acc.plus(money) }\n\n    fun toIssuedTicketInfoVo(): IssuedTicketInfoVo = IssuedTicketInfoVo.from(this)\n\n    fun toEmailIssuedTicketInfo(): EmailIssuedTicketInfo = EmailIssuedTicketInfo(\n        this.issuedTicketNo!!,\n        this.itemInfo!!.ticketName!!,\n        createdAtKt(),\n        this.issuedTicketStatus.kr,\n        this.itemInfo!!.price.toString(),\n    )\n\n    /** ---------------------------- 상태 변환 관련 메서드 ---------------------------------- */\n\n    fun cancel() {\n        if (!this.issuedTicketStatus.isBeforeEntrance()) {\n            throw CanNotCancelException.EXCEPTION\n        }\n        this.issuedTicketStatus = IssuedTicketStatus.CANCELED\n    }\n\n    fun entrance() {\n        if (this.issuedTicketStatus.isCanceled()) {\n            throw CanNotEntranceException.EXCEPTION\n        }\n        if (this.issuedTicketStatus.isAfterEntrance()) {\n            throw IssuedTicketAlreadyEntranceException.EXCEPTION\n        }\n        this.issuedTicketStatus = IssuedTicketStatus.ENTRANCE_COMPLETED\n        this.enteredAt = LocalDateTime.now()\n        Events.raise(EntranceIssuedTicketEvent.from(this))\n    }\n\n    fun entranceCancel() {\n        if (!this.issuedTicketStatus.isAfterEntrance()) {\n            throw CanNotCancelEntranceException.EXCEPTION\n        }\n        this.issuedTicketStatus = IssuedTicketStatus.ENTRANCE_INCOMPLETE\n    }\n\n    fun getUserId(): Long? = this.userInfo?.userId\n\n    companion object {\n        @JvmStatic\n        fun create(\n            ticketItem: TicketItem,\n            user: User,\n            order: Order,\n            eventId: Long?,\n            orderLineItem: OrderLineItem,\n        ): IssuedTicket {\n            val orderOptionAnswers = orderLineItem.orderOptionAnswers\n            return IssuedTicket(\n                initialOptionAnswers = orderOptionAnswers.map { IssuedTicketOptionAnswer.from(it) },\n                itemInfo = IssuedTicketItemInfoVo.from(ticketItem),\n                orderLineId = orderLineItem.id,\n                orderUuid = order.uuid,\n                issuedTicketStatus = IssuedTicketStatus.ENTRANCE_INCOMPLETE,\n                userInfo = IssuedTicketUserInfoVo.from(user),\n                eventId = eventId,\n            )\n        }\n\n        @JvmStatic\n        fun orderLineToIssuedTicket(\n            ticketItem: TicketItem,\n            user: User,\n            order: Order,\n            eventId: Long?,\n            orderLineItem: OrderLineItem,\n        ): List<IssuedTicket> {\n            val quantity = orderLineItem.quantity!!\n            return (0 until quantity).map { create(ticketItem, user, order, eventId, orderLineItem) }\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/domain/IssuedTicketCancelReason.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.domain\n\nimport com.fasterxml.jackson.annotation.JsonValue\n\nenum class IssuedTicketCancelReason(\n    val value: String,\n    @JsonValue val kr: String,\n) {\n    REFUND(\"REFUND\", \"사용자에 의한 환불\")\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/domain/IssuedTicketItemInfoVo.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.domain\n\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport band.gosrock.domain.domains.ticket_item.domain.TicketPayType\nimport band.gosrock.domain.domains.ticket_item.domain.TicketType\nimport jakarta.persistence.Embeddable\nimport jakarta.persistence.EnumType\nimport jakarta.persistence.Enumerated\n\n@Embeddable\nclass IssuedTicketItemInfoVo(\n    var ticketItemId: Long? = null,\n\n    @Enumerated(EnumType.STRING)\n    var ticketType: TicketType? = null,\n\n    @Enumerated(EnumType.STRING)\n    var payType: TicketPayType? = null,\n\n    var ticketName: String? = null,\n\n    var price: Money? = null,\n) {\n    companion object {\n        @JvmStatic\n        fun from(item: TicketItem): IssuedTicketItemInfoVo = IssuedTicketItemInfoVo(\n            ticketItemId = item.id,\n            ticketType = item.type,\n            payType = item.payType,\n            ticketName = item.name,\n            price = item.price,\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/domain/IssuedTicketOptionAnswer.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.domain\n\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport band.gosrock.domain.common.vo.IssuedTicketOptionAnswerVo\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.common.vo.OptionAnswerVo\nimport band.gosrock.domain.domains.order.domain.OrderOptionAnswer\nimport band.gosrock.domain.domains.ticket_item.domain.Option\nimport jakarta.persistence.Column\nimport jakarta.persistence.Entity\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\n\n@Entity(name = \"tbl_issued_ticket_option_answer\")\nclass IssuedTicketOptionAnswer(\n    var optionId: Long? = null,\n    var additionalPrice: Money = Money.ZERO,\n    var answer: String? = null,\n) : BaseTimeEntity() {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"issued_ticket_option_answer_id\")\n    var id: Long? = null\n        protected set\n\n    companion object {\n        @JvmStatic\n        fun from(orderOptionAnswer: OrderOptionAnswer): IssuedTicketOptionAnswer =\n            IssuedTicketOptionAnswer(\n                optionId = orderOptionAnswer.optionId,\n                additionalPrice = orderOptionAnswer.additionalPrice,\n                answer = orderOptionAnswer.answer,\n            )\n    }\n\n    fun toIssuedTicketOptionAnswerVo(): IssuedTicketOptionAnswerVo =\n        IssuedTicketOptionAnswerVo.from(this)\n\n    fun getOptionAnswerVo(option: Option): OptionAnswerVo =\n        OptionAnswerVo(\n            questionDescription = option.getQuestionDescription(),\n            optionGroupType = option.getQuestionType(),\n            questionName = option.getQuestionName(),\n            answer = answer,\n            additionalPrice = option.additionalPrice,\n        )\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/domain/IssuedTicketStatus.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.domain\n\nimport com.fasterxml.jackson.annotation.JsonValue\n\nenum class IssuedTicketStatus(\n    val value: String,\n    @JsonValue val kr: String,\n) {\n    ENTRANCE_COMPLETED(\"ENTRANCE_COMPLETED\", \"입장 완료\"),\n    ENTRANCE_INCOMPLETE(\"ENTRANCE_INCOMPLETE\", \"입장 전\"),\n    CANCELED(\"CANCELED\", \"취소 티켓\");\n\n    fun isCanceled(): Boolean = this == CANCELED\n\n    fun isBeforeEntrance(): Boolean = this == ENTRANCE_INCOMPLETE\n\n    fun isAfterEntrance(): Boolean = this == ENTRANCE_COMPLETED\n\n    fun `is`(issuedTicket: IssuedTicket): Boolean =\n        issuedTicket.issuedTicketStatus == ENTRANCE_INCOMPLETE\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/domain/IssuedTicketUserInfoVo.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.domain\n\nimport band.gosrock.domain.common.vo.PhoneNumberVo\nimport band.gosrock.domain.domains.user.domain.User\nimport jakarta.persistence.Embeddable\n\n@Embeddable\nclass IssuedTicketUserInfoVo(\n    var userId: Long? = null,\n    var userName: String? = null,\n    var email: String? = null,\n    var phoneNumber: PhoneNumberVo? = null,\n) {\n    companion object {\n        @JvmStatic\n        fun from(user: User): IssuedTicketUserInfoVo = IssuedTicketUserInfoVo(\n            userId = user.id,\n            userName = user.profile?.name,\n            phoneNumber = user.profile?.phoneNumberVo,\n            email = user.profile?.email,\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/domain/IssuedTickets.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.domain\n\nimport band.gosrock.domain.common.vo.IssuedTicketInfoVo\nimport band.gosrock.domain.domains.event.domain.Event\n\nclass IssuedTickets(val issuedTickets: List<IssuedTicket>) {\n\n    companion object {\n        @JvmStatic\n        fun from(issuedTickets: List<IssuedTicket>): IssuedTickets = IssuedTickets(issuedTickets)\n    }\n\n    fun getNos(): List<String?> = issuedTickets.map { it.issuedTicketNo }\n\n    fun getTotalQuantity(): Int = issuedTickets.size\n\n    fun getTicketNoName(): String {\n        val nos = getNos()\n        val size = nos.size\n        return when {\n            size == 0 -> \"\"\n            size == 1 -> String.format(\"%s (%d매)\", nos[0], size)\n            else -> String.format(\"%s ~ %s (%d매)\", nos[0], nos[size - 1], size)\n        }\n    }\n\n    fun getIssuedTicketsStage(event: Event): IssuedTicketsStage {\n        if (getTotalQuantity() == 0) return IssuedTicketsStage.APPROVE_WAITING\n        val issuedTicketStatuses = getIssuedTicketStatuses()\n        if (isCanceled(issuedTicketStatuses)) return IssuedTicketsStage.CANCELED\n        if (event.isClosed()) return IssuedTicketsStage.PASSED_EVENT\n        if (isBeforeEntrance(issuedTicketStatuses)) return IssuedTicketsStage.BEFORE_ENTRANCE\n        if (isAfterEntrance(issuedTicketStatuses)) return IssuedTicketsStage.AFTER_ENTRANCE\n        return IssuedTicketsStage.ENTERING\n    }\n\n    fun getIssuedTicketStatuses(): List<IssuedTicketStatus> =\n        issuedTickets.map { it.issuedTicketStatus }\n\n    fun getIssuedTicketInfoVos(): List<IssuedTicketInfoVo> =\n        issuedTickets.map { it.toIssuedTicketInfoVo() }\n\n    private fun isCanceled(statuses: List<IssuedTicketStatus>): Boolean =\n        statuses.any { it.isCanceled() }\n\n    private fun isBeforeEntrance(statuses: List<IssuedTicketStatus>): Boolean =\n        statuses.all { it.isBeforeEntrance() }\n\n    private fun isAfterEntrance(statuses: List<IssuedTicketStatus>): Boolean =\n        statuses.all { it.isAfterEntrance() }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/domain/IssuedTicketsStage.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.domain\n\nimport com.fasterxml.jackson.annotation.JsonValue\n\nenum class IssuedTicketsStage(\n    val value: String,\n    @JsonValue val kr: String,\n) {\n    APPROVE_WAITING(\"APPROVE_WAITING\", \"승인대기\"),\n    AFTER_ENTRANCE(\"AFTER_ENTRANCE\", \"입장완료\"),\n    BEFORE_ENTRANCE(\"BEFORE_ENTRANCE\", \"관람예정\"),\n    ENTERING(\"ENTERING\", \"입장중\"),\n    CANCELED(\"CANCELED\", \"취소됨\"),\n    PASSED_EVENT(\"PASSED_EVENT\", \"지난공연\")\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/dto/condition/IssuedTicketCondition.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.dto.condition\n\nclass IssuedTicketCondition(\n    val eventId: Long,\n    val userName: String?,\n    val phoneNumber: String?,\n)\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/dto/request/CreateIssuedTicketDTO.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.dto.request\n\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderLineItem\nimport band.gosrock.domain.domains.user.domain.User\n\nclass CreateIssuedTicketDTO(\n    val order: Order,\n    val orderLineItem: OrderLineItem,\n    val user: User,\n)\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/dto/request/CreateIssuedTicketForDevDTO.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.dto.request\n\nimport org.jetbrains.annotations.NotNull\n\nclass CreateIssuedTicketForDevDTO {\n    @NotNull\n    var eventId: Long? = null\n\n    @NotNull\n    var orderLineId: Long? = null\n\n    @NotNull\n    var ticketItemId: Long? = null\n\n    @NotNull\n    var amount: Long? = null\n\n    @NotNull\n    var optionAnswers: List<Long>? = null\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/dto/request/CreateIssuedTicketRequestDTOs.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.dto.request\n\nclass CreateIssuedTicketRequestDTOs {\n    var createIssuedTicketRequestForDevs: List<CreateIssuedTicketRequestForDev>? = null\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/dto/request/CreateIssuedTicketRequestForDev.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.dto.request\n\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketOptionAnswer\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport band.gosrock.domain.domains.user.domain.User\n\nclass CreateIssuedTicketRequestForDev(\n    val event: Event,\n    val orderLineId: Long,\n    val user: User,\n    val price: Money,\n    val ticketItem: TicketItem,\n    val issuedTicketOptionAnswers: List<IssuedTicketOptionAnswer>,\n)\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/dto/response/CreateIssuedTicketResponse.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.dto.response\n\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketOptionAnswer\n\nclass CreateIssuedTicketResponse(\n    val issuedTickets: List<IssuedTicket>,\n    val issuedTicketOptionAnswers: List<IssuedTicketOptionAnswer>,\n)\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/dto/response/IssuedTicketDTO.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.dto.response\n\nimport band.gosrock.domain.common.vo.EventInfoVo\nimport band.gosrock.domain.common.vo.IssuedTicketInfoVo\n\nclass IssuedTicketDTO private constructor(\n    val issuedTicketInfo: IssuedTicketInfoVo,\n    val eventInfo: EventInfoVo,\n) {\n    companion object {\n        @JvmStatic\n        fun builder() = Builder()\n    }\n\n    class Builder {\n        private var issuedTicketInfo: IssuedTicketInfoVo? = null\n        private var eventInfo: EventInfoVo? = null\n\n        fun issuedTicketInfo(issuedTicketInfo: IssuedTicketInfoVo) = apply { this.issuedTicketInfo = issuedTicketInfo }\n        fun eventInfo(eventInfo: EventInfoVo) = apply { this.eventInfo = eventInfo }\n\n        fun build(): IssuedTicketDTO = IssuedTicketDTO(issuedTicketInfo!!, eventInfo!!)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/dto/response/IssuedTicketPageDTO.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.dto.response\n\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport org.springframework.data.domain.Page\n\nclass IssuedTicketPageDTO {\n    var issuedTickets: Page<IssuedTicket>? = null\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/exception/CanNotCancelEntranceException.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CanNotCancelEntranceException private constructor() : DuDoongCodeException(IssuedTicketErrorCode.CAN_NOT_CANCEL_ENTRANCE) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CanNotCancelEntranceException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/exception/CanNotCancelException.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CanNotCancelException private constructor() : DuDoongCodeException(IssuedTicketErrorCode.CAN_NOT_CANCEL) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CanNotCancelException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/exception/CanNotEntranceException.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CanNotEntranceException private constructor() : DuDoongCodeException(IssuedTicketErrorCode.CAN_NOT_ENTRANCE) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CanNotEntranceException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/exception/IssuedTicketAlreadyEntranceException.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass IssuedTicketAlreadyEntranceException private constructor() : DuDoongCodeException(IssuedTicketErrorCode.ISSUED_TICKET_ALREADY_ENTRANCE) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = IssuedTicketAlreadyEntranceException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/exception/IssuedTicketErrorCode.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.exception\n\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.consts.DuDoongStatic.BAD_REQUEST\nimport band.gosrock.common.consts.DuDoongStatic.NOT_FOUND\nimport band.gosrock.common.dto.ErrorReason\nimport band.gosrock.common.exception.BaseErrorCode\nimport java.lang.reflect.Field\n\nenum class IssuedTicketErrorCode(\n    private val status: Int,\n    private val code: String,\n    private val reason: String,\n) : BaseErrorCode {\n    ISSUED_TICKET_NOT_FOUND(NOT_FOUND, \"IssuedTicket_404_1\", \"티켓을 찾을 수 없습니다.\"),\n    ISSUED_TICKET_NOT_MATCHED_USER(BAD_REQUEST, \"IssuedTicket_400_1\", \"IssuedTicket User Not Matched\"),\n    CAN_NOT_CANCEL(BAD_REQUEST, \"IssuedTicket_400_2\", \"티켓을 취소 할 수 있는 상태가 아닙니다.\"),\n    CAN_NOT_CANCEL_ENTRANCE(BAD_REQUEST, \"IssuedTicket_400_3\", \"티켓이 입장 취소 할 수 있는 상태가 아닙니다.\"),\n    CAN_NOT_ENTRANCE(BAD_REQUEST, \"IssuedTicket_400_4\", \"티켓이 입장 할 수 있는 상태가 아닙니다.\"),\n    ISSUED_TICKET_ALREADY_ENTRANCE(BAD_REQUEST, \"IssuedTicket_400_5\", \"이미 입장 처리된 티켓입니다.\"),\n    ISSUED_TICKET_NOT_MATCHED_EVENT(BAD_REQUEST, \"IssuedTicket_400_6\", \"이 티켓은 해당 이벤트에서 발급된 티켓이 아닙니다.\");\n\n    override fun getErrorReason(): ErrorReason =\n        ErrorReason(status = status, code = code, reason = reason)\n\n    override fun getExplainError(): String {\n        val field: Field = this.javaClass.getField(this.name)\n        val annotation: ExplainError? = field.getAnnotation(ExplainError::class.java)\n        return annotation?.value ?: this.reason\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/exception/IssuedTicketNotFoundException.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass IssuedTicketNotFoundException private constructor() : DuDoongCodeException(IssuedTicketErrorCode.ISSUED_TICKET_NOT_FOUND) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = IssuedTicketNotFoundException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/exception/IssuedTicketNotMatchedEventException.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass IssuedTicketNotMatchedEventException private constructor() : DuDoongCodeException(IssuedTicketErrorCode.ISSUED_TICKET_NOT_MATCHED_EVENT) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = IssuedTicketNotMatchedEventException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/exception/IssuedTicketUserNotMatchedException.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass IssuedTicketUserNotMatchedException private constructor() : DuDoongCodeException(IssuedTicketErrorCode.ISSUED_TICKET_NOT_MATCHED_USER) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = IssuedTicketUserNotMatchedException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/repository/IssuedTicketCustomRepository.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.repository\n\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport band.gosrock.domain.domains.issuedTicket.repository.condition.FindEventIssuedTicketsCondition\nimport java.util.Optional\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\n\ninterface IssuedTicketCustomRepository {\n    fun searchToPage(condition: FindEventIssuedTicketsCondition, pageable: Pageable): Page<IssuedTicket>\n    fun find(issuedTicketId: Long): Optional<IssuedTicket>\n    fun countPaidTicket(userId: Long, issuedTicketId: Long): Long\n    fun countIssuedTicketByItemId(ticketItemId: Long): Long\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/repository/IssuedTicketCustomRepositoryImpl.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.repository\n\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketStatus\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketStatus.ENTRANCE_COMPLETED\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketStatus.ENTRANCE_INCOMPLETE\nimport band.gosrock.domain.domains.issuedTicket.domain.QIssuedTicket.issuedTicket\nimport band.gosrock.domain.domains.issuedTicket.domain.QIssuedTicketOptionAnswer.issuedTicketOptionAnswer\nimport band.gosrock.domain.domains.issuedTicket.repository.condition.FindEventIssuedTicketsCondition\nimport band.gosrock.domain.domains.user.domain.QUser.user\nimport com.querydsl.core.types.ExpressionUtils.count\nimport com.querydsl.core.types.dsl.BooleanExpression\nimport com.querydsl.jpa.impl.JPAQuery\nimport com.querydsl.jpa.impl.JPAQueryFactory\nimport java.util.Optional\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.support.PageableExecutionUtils\n\nclass IssuedTicketCustomRepositoryImpl(\n    private val queryFactory: JPAQueryFactory\n) : IssuedTicketCustomRepository {\n\n    override fun searchToPage(condition: FindEventIssuedTicketsCondition, pageable: Pageable): Page<IssuedTicket> {\n        val issuedTickets = queryFactory\n            .selectFrom(issuedTicket)\n            .join(user)\n            .on(user.id.eq(issuedTicket.userInfo.userId))\n            .where(\n                eventIdEq(condition.eventId),\n                condition.getSearchStringFilter(),\n                issuedTicketStatusNotCanceled()\n            )\n            .orderBy(issuedTicket.id.desc())\n            .offset(pageable.offset)\n            .limit(pageable.pageSize.toLong())\n            .fetch()\n\n        val countQuery: JPAQuery<Long> = queryFactory\n            .select(issuedTicket.count())\n            .from(issuedTicket)\n            .join(user)\n            .on(user.id.eq(issuedTicket.userInfo.userId))\n            .where(\n                eventIdEq(condition.eventId),\n                condition.getSearchStringFilter(),\n                issuedTicketStatusNotCanceled()\n            )\n\n        return PageableExecutionUtils.getPage(issuedTickets, pageable) { countQuery.fetchOne() ?: 0L }\n    }\n\n    override fun find(issuedTicketId: Long): Optional<IssuedTicket> {\n        val findIssuedTicket = queryFactory\n            .selectFrom(issuedTicket)\n            .leftJoin(issuedTicket.issuedTicketOptionAnswers, issuedTicketOptionAnswer)\n            .fetchJoin()\n            .where(issuedTicket.id.eq(issuedTicketId), issuedTicketStatusNotCanceled())\n            .fetchOne()\n        return Optional.ofNullable(findIssuedTicket)\n    }\n\n    override fun countPaidTicket(userId: Long, issuedTicketId: Long): Long {\n        val result = queryFactory\n            .select(count(issuedTicket))\n            .from(issuedTicket)\n            .where(eqUserId(userId), eqTicketItemId(issuedTicketId), filterPaidTickets())\n            .fetchOne()\n        return result ?: 0L\n    }\n\n    override fun countIssuedTicketByItemId(ticketItemId: Long): Long {\n        val result = queryFactory\n            .select(count(issuedTicket))\n            .from(issuedTicket)\n            .where(eqTicketItemId(ticketItemId), filterPaidTickets())\n            .fetchOne()\n        return result ?: 0L\n    }\n\n    private fun filterPaidTickets(): BooleanExpression =\n        issuedTicket.issuedTicketStatus.`in`(ENTRANCE_COMPLETED, ENTRANCE_INCOMPLETE)\n\n    private fun eqTicketItemId(ticketItemId: Long): BooleanExpression =\n        issuedTicket.itemInfo.ticketItemId.eq(ticketItemId)\n\n    private fun eqUserId(userId: Long): BooleanExpression =\n        issuedTicket.userInfo.userId.eq(userId)\n\n    private fun eventIdEq(eventId: Long?): BooleanExpression? =\n        if (eventId == null) null else issuedTicket.eventId.eq(eventId)\n\n    private fun issuedTicketStatusNotCanceled(): BooleanExpression =\n        issuedTicket.issuedTicketStatus.eq(IssuedTicketStatus.CANCELED).not()\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/repository/IssuedTicketOptionAnswerRepository.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.repository\n\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketOptionAnswer\nimport org.springframework.data.jpa.repository.JpaRepository\n\ninterface IssuedTicketOptionAnswerRepository : JpaRepository<IssuedTicketOptionAnswer, Long>\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/repository/IssuedTicketRepository.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.repository\n\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport java.util.Optional\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.jpa.repository.JpaRepository\n\ninterface IssuedTicketRepository : JpaRepository<IssuedTicket, Long>, IssuedTicketCustomRepository {\n    fun findAllByOrderLineId(orderLineId: Long): List<IssuedTicket>\n    fun findAllByOrderUuid(orderId: String): List<IssuedTicket>\n    fun findByIssuedTicketNo(issuedTicketNo: String): Optional<IssuedTicket>\n    fun existsByEventId(eventId: Long): Boolean\n    fun countByEventId(eventId: Long): Long\n    fun findAllByEventId(eventId: Long, pageable: Pageable): Page<IssuedTicket>\n    fun findAllByEventId(eventId: Long): List<IssuedTicket>\n    fun findByUuid(uuid: String): Optional<IssuedTicket>\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/repository/condition/FindEventIssuedTicketsCondition.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.repository.condition\n\nimport band.gosrock.domain.domains.order.repository.condition.AdminTableSearchType\nimport com.querydsl.core.types.dsl.BooleanExpression\n\nclass FindEventIssuedTicketsCondition @JvmOverloads constructor(\n    val eventId: Long,\n    searchString: String? = null,\n    val searchType: AdminTableSearchType?,\n) {\n    val searchString: String = searchString ?: \"\"\n\n    fun getSearchStringFilter(): BooleanExpression? {\n        if (searchType == null) return null\n        return searchType.getContains(searchString)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/service/IssuedTicketDomainService.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.common.aop.redissonLock.RedissonLock\nimport band.gosrock.domain.common.vo.IssuedTicketInfoVo\nimport band.gosrock.domain.domains.issuedTicket.adaptor.IssuedTicketAdaptor\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport band.gosrock.domain.domains.issuedTicket.validator.IssuedTicketValidator\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@DomainService\n@Transactional(readOnly = true)\nclass IssuedTicketDomainService(\n    private val issuedTicketAdaptor: IssuedTicketAdaptor,\n    private val ticketItemAdaptor: TicketItemAdaptor,\n    private val userAdaptor: UserAdaptor,\n    private val orderAdaptor: OrderAdaptor,\n    private val issuedTicketValidator: IssuedTicketValidator,\n) {\n\n    @RedissonLock(LockName = \"티켓관리\", identifier = \"itemId\")\n    fun withdrawIssuedTicket(itemId: Long, issuedTickets: List<IssuedTicket>) {\n        val ticketItem = ticketItemAdaptor.queryTicketItem(itemId)\n        issuedTickets.forEach { issuedTicket ->\n            ticketItem.increaseQuantity(1L)\n            issuedTicket.cancel()\n        }\n    }\n\n    @RedissonLock(LockName = \"티켓관리\", identifier = \"itemId\")\n    fun doneOrderEventAfterRollBackWithdrawIssuedTickets(itemId: Long, orderUuid: String) {\n        val failIssuedTickets = issuedTicketAdaptor.findAllByOrderUuid(orderUuid)\n        val ticketItem = ticketItemAdaptor.queryTicketItem(itemId)\n        failIssuedTickets.forEach { issuedTicket ->\n            ticketItem.increaseQuantity(1L)\n            issuedTicket.cancel()\n        }\n    }\n\n    fun processingEntranceIssuedTicket(eventId: Long, uuid: String): IssuedTicketInfoVo {\n        val issuedTicket = issuedTicketAdaptor.queryByIssuedTicketUuid(uuid)\n        issuedTicketValidator.validIssuedTicketEventIdEqualEvent(issuedTicket, eventId)\n        issuedTicket.entrance()\n        return issuedTicket.toIssuedTicketInfoVo()\n    }\n\n    @RedissonLock(LockName = \"티켓관리\", identifier = \"itemId\")\n    fun createIssuedTicket(itemId: Long, orderUuid: String, userId: Long) {\n        val ticketItem = ticketItemAdaptor.queryTicketItem(itemId)\n        val user = userAdaptor.queryUser(userId)\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        val orderLineItems = order.orderLineItems\n        val issuedTickets = orderLineItems.flatMap { orderLineItem ->\n            ticketItem.reduceQuantity(orderLineItem.quantity)\n            IssuedTicket.orderLineToIssuedTicket(ticketItem, user, order, order.eventId, orderLineItem)\n        }\n        issuedTicketAdaptor.saveAll(issuedTickets)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/service/OrderToIssuedTicketService.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\n\n@DomainService\nclass OrderToIssuedTicketService(\n    private val userAdaptor: UserAdaptor,\n    private val orderAdaptor: OrderAdaptor,\n) {\n    fun execute(ticketItem: TicketItem, orderUuid: String, userId: Long): List<IssuedTicket> {\n        val user = userAdaptor.queryUser(userId)\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        val eventId = ticketItem.eventId\n        return order.orderLineItems.flatMap { orderLineItem ->\n            val quantity = orderLineItem.quantity!!\n            (0 until quantity).map {\n                IssuedTicket.create(ticketItem, user, order, eventId, orderLineItem)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/service/handlers/OrderEventHandler.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.service.handlers\n\nimport band.gosrock.domain.common.events.order.DoneOrderEvent\nimport band.gosrock.domain.domains.issuedTicket.service.IssuedTicketDomainService\nimport org.slf4j.LoggerFactory\nimport org.springframework.context.event.EventListener\nimport org.springframework.stereotype.Component\n\n@Component\nclass OrderEventHandler(\n    private val issuedTicketDomainService: IssuedTicketDomainService,\n) {\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    @EventListener(classes = [DoneOrderEvent::class])\n    fun handleDoneOrderEvent(doneOrderEvent: DoneOrderEvent) {\n        log.info(\"${doneOrderEvent.orderUuid}주문 상태 완료, 티켓 생성작업 진행\")\n        issuedTicketDomainService.createIssuedTicket(\n            doneOrderEvent.itemId,\n            doneOrderEvent.orderUuid,\n            doneOrderEvent.userId,\n        )\n        log.info(\"${doneOrderEvent.orderUuid}주문 상태 완료, 티켓 생성작업 완료\")\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/service/handlers/WithdrawOrderEventHandler.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.service.handlers\n\nimport band.gosrock.domain.common.events.order.WithDrawOrderEvent\nimport band.gosrock.domain.domains.issuedTicket.adaptor.IssuedTicketAdaptor\nimport band.gosrock.domain.domains.issuedTicket.service.IssuedTicketDomainService\nimport org.slf4j.LoggerFactory\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\n@Component\nclass WithdrawOrderEventHandler(\n    private val issuedTicketDomainService: IssuedTicketDomainService,\n    private val issuedTicketAdaptor: IssuedTicketAdaptor,\n) {\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    @TransactionalEventListener(\n        classes = [WithDrawOrderEvent::class],\n        phase = TransactionPhase.BEFORE_COMMIT,\n    )\n    fun handleWithdrawOrderEvent(withDrawOrderEvent: WithDrawOrderEvent) {\n        log.info(\"${withDrawOrderEvent.orderUuid}주문 상태 철회 , 티켓 철회 필요\")\n        val issuedTickets = issuedTicketAdaptor.findAllByOrderUuid(withDrawOrderEvent.orderUuid)\n        issuedTicketDomainService.withdrawIssuedTicket(\n            withDrawOrderEvent.itemId, issuedTickets,\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/issuedTicket/validator/IssuedTicketValidator.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.validator\n\nimport band.gosrock.common.annotation.Validator\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport band.gosrock.domain.domains.issuedTicket.exception.IssuedTicketNotMatchedEventException\n\n@Validator\nclass IssuedTicketValidator {\n\n    fun validIssuedTicketEventIdEqualEvent(issuedTicket: IssuedTicket, eventId: Long) {\n        if (issuedTicket.eventId != eventId) {\n            throw IssuedTicketNotMatchedEventException.EXCEPTION\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/adaptor/OrderAdaptor.kt",
    "content": "package band.gosrock.domain.domains.order.adaptor\n\nimport band.gosrock.common.annotation.Adaptor\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport band.gosrock.domain.domains.order.domain.RefundStatus\nimport band.gosrock.domain.domains.order.exception.OrderNotFoundException\nimport band.gosrock.domain.domains.order.repository.OrderRepository\nimport band.gosrock.domain.domains.order.repository.condition.FindEventOrdersCondition\nimport band.gosrock.domain.domains.order.repository.condition.FindMyPageOrderCondition\nimport java.util.Optional\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.domain.Slice\n\n@Adaptor\nclass OrderAdaptor(private val orderRepository: OrderRepository) {\n\n    fun save(order: Order): Order = orderRepository.save(order)\n\n    fun findById(orderId: Long): Order =\n        orderRepository.findById(orderId).orElseThrow { OrderNotFoundException.EXCEPTION }\n\n    fun findByEventId(eventId: Long): List<Order> =\n        orderRepository.findByEventId(eventId)\n\n    fun findByOrderUuid(uuid: String): Order =\n        orderRepository.findByOrderUuid(uuid).orElseThrow { OrderNotFoundException.EXCEPTION }\n\n    fun findByUuidIn(orderUuids: List<String>): List<Order> =\n        orderRepository.findByUuidIn(orderUuids)\n\n    fun findRecentOrderByUserId(userId: Long): Optional<Order> =\n        orderRepository.findRecentOrder(userId)\n\n    fun findMyOrders(condition: FindMyPageOrderCondition, pageable: Pageable): Slice<Order> =\n        orderRepository.findMyOrders(condition, pageable)\n\n    fun findEventOrders(condition: FindEventOrdersCondition, pageable: Pageable): Page<Order> =\n        orderRepository.findEventOrders(condition, pageable)\n\n    fun findByEventIdAndOrderStatus(eventId: Long, orderStatus: OrderStatus): List<Order> =\n        orderRepository.findByEventIdAndOrderStatus(eventId, orderStatus)\n\n    fun findByEventIdAndOrderStatusAndUserId(eventId: Long, userId: Long, orderStatus: OrderStatus): List<Order> =\n        orderRepository.findByEventIdAndUserIdAndOrderStatus(eventId, userId, orderStatus)\n\n    fun findRefunds(eventId: Long?, refundStatus: RefundStatus?, keyword: String?, pageable: Pageable): Page<Order> =\n        orderRepository.findRefunds(eventId, refundStatus, keyword, pageable)\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/domain/Order.kt",
    "content": "package band.gosrock.domain.domains.order.domain\n\nimport band.gosrock.common.consts.DuDoongStatic.NO_START_NUMBER\nimport band.gosrock.domain.common.aop.domainEvent.Events\nimport band.gosrock.domain.common.events.order.CreateOrderEvent\nimport band.gosrock.domain.common.events.order.DoneOrderEvent\nimport band.gosrock.domain.common.events.order.WithDrawOrderEvent\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.cart.domain.Cart\nimport band.gosrock.domain.domains.coupon.domain.IssuedCoupon\nimport band.gosrock.domain.domains.order.domain.validator.OrderValidator\nimport band.gosrock.domain.domains.order.exception.InvalidOrderException\nimport band.gosrock.domain.domains.order.exception.NotPaymentOrderException\nimport band.gosrock.domain.domains.order.exception.OrderLineNotFountException\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport band.gosrock.infrastructure.config.alilmTalk.dto.AlimTalkOrderInfo\nimport band.gosrock.infrastructure.config.mail.dto.EmailOrderInfo\nimport java.time.LocalDateTime\nimport java.util.UUID\nimport jakarta.persistence.CascadeType\nimport jakarta.persistence.Column\nimport jakarta.persistence.Embedded\nimport jakarta.persistence.Entity\nimport jakarta.persistence.EnumType\nimport jakarta.persistence.Enumerated\nimport jakarta.persistence.FetchType\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\nimport jakarta.persistence.JoinColumn\nimport jakarta.persistence.OneToMany\nimport jakarta.persistence.PostPersist\nimport jakarta.persistence.PrePersist\n\n@Entity(name = \"tbl_order\")\nclass Order() : BaseTimeEntity() {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"order_id\")\n    var id: Long? = null\n        protected set\n\n    @Column(nullable = false)\n    var userId: Long? = null\n        protected set\n\n    @Column(nullable = false)\n    var eventId: Long? = null\n        protected set\n\n    @Column(nullable = false)\n    var uuid: String? = null\n        protected set\n\n    var orderNo: String? = null\n        protected set\n\n    @Column(nullable = false)\n    var orderName: String? = null\n        protected set\n\n    @Embedded\n    var pgPaymentInfo: PgPaymentInfo = PgPaymentInfo.empty()\n        protected set\n\n    var approvedAt: LocalDateTime? = null\n        protected set\n\n    var withDrawAt: LocalDateTime? = null\n        protected set\n\n    @Enumerated(EnumType.STRING)\n    @Column(nullable = false)\n    var orderMethod: OrderMethod? = null\n        protected set\n\n    @Embedded\n    var totalPaymentInfo: PaymentInfo? = null\n        protected set\n\n    @Enumerated(EnumType.STRING)\n    @Column(nullable = false)\n    var orderStatus: OrderStatus = OrderStatus.READY\n        protected set\n\n    @Embedded\n    var orderCouponVo: OrderCouponVo = OrderCouponVo.empty()\n        protected set\n\n    @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY)\n    @JoinColumn(name = \"order_id\")\n    var orderLineItems: MutableList<OrderLineItem> = mutableListOf()\n        protected set\n\n    @Column(length = 500)\n    var failReason: String? = null\n        protected set\n\n    @Column(length = 500)\n    var cancelReason: String? = null\n        protected set\n\n    @Enumerated(EnumType.STRING)\n    @Column(length = 30)\n    var refundStatus: RefundStatus = RefundStatus.NONE\n        protected set\n\n    var refundStatusChangedAt: LocalDateTime? = null\n        protected set\n\n    @PrePersist\n    fun addUUID() {\n        uuid = UUID.randomUUID().toString()\n    }\n\n    @PostPersist\n    fun createOrder() {\n        orderNo = \"R\" + (NO_START_NUMBER + id!!)\n        Events.raise(CreateOrderEvent.from(this))\n    }\n\n    companion object {\n        @JvmStatic\n        fun createPaymentOrder(userId: Long, cart: Cart, item: TicketItem, orderValidator: OrderValidator): Order {\n            val order = Order().apply {\n                this.userId = userId\n                this.orderName = cart.cartName\n                this.orderLineItems.addAll(getOrderLineItems(cart, item))\n                this.orderStatus = OrderStatus.PENDING_PAYMENT\n                this.orderMethod = OrderMethod.PAYMENT\n                this.eventId = item.eventId\n            }\n            orderValidator.validCanCreate(order)\n            order.calculatePaymentInfo()\n            return order\n        }\n\n        @JvmStatic\n        fun createApproveOrder(userId: Long, cart: Cart, item: TicketItem, orderValidator: OrderValidator): Order {\n            val order = Order().apply {\n                this.userId = userId\n                this.orderName = cart.cartName\n                this.orderLineItems.addAll(getOrderLineItems(cart, item))\n                this.orderStatus = OrderStatus.PENDING_APPROVE\n                this.orderMethod = OrderMethod.APPROVAL\n                this.eventId = item.eventId\n            }\n            orderValidator.validCanCreate(order)\n            orderValidator.validApproveStatePurchaseLimit(order)\n            orderValidator.validApproveOrderCreateTotalStock(order)\n            order.calculatePaymentInfo()\n            return order\n        }\n\n        @JvmStatic\n        fun createPaymentOrderWithCoupon(\n            userId: Long,\n            cart: Cart,\n            item: TicketItem,\n            coupon: IssuedCoupon,\n            orderValidator: OrderValidator,\n        ): Order {\n            if (!item.isFCFS() || !cart.isNeedPaid()) {\n                throw InvalidOrderException.EXCEPTION\n            }\n            val supplyAmount = cart.getTotalPrice()\n            val couponVo = OrderCouponVo.of(coupon, supplyAmount)\n            couponVo.validMinimumPaymentAmount(supplyAmount)\n            val order = createPaymentOrder(userId, cart, item, orderValidator)\n            order.attachCoupon(couponVo)\n            order.calculatePaymentInfo()\n            return order\n        }\n\n        private fun getOrderLineItems(cart: Cart, item: TicketItem): List<OrderLineItem> =\n            cart.cartLineItems.map { OrderLineItem.of(it, item) }\n\n        /** 테스트 전용 팩토리 메서드 */\n        @JvmStatic\n        @JvmOverloads\n        fun forTest(\n            userId: Long? = null,\n            orderName: String? = null,\n            orderLineItems: List<OrderLineItem> = emptyList(),\n            orderStatus: OrderStatus = OrderStatus.READY,\n            orderMethod: OrderMethod? = null,\n            eventId: Long? = null,\n            failReason: String? = null,\n            cancelReason: String? = null,\n            refundStatus: RefundStatus = RefundStatus.NONE,\n        ): Order = Order().apply {\n            this.userId = userId\n            this.orderName = orderName\n            this.orderLineItems.addAll(orderLineItems)\n            this.orderStatus = orderStatus\n            this.orderMethod = orderMethod\n            this.eventId = eventId\n            this.failReason = failReason\n            this.cancelReason = cancelReason\n            this.refundStatus = refundStatus\n        }\n    }\n\n    fun calculatePaymentInfo() {\n        totalPaymentInfo = PaymentInfo.of(\n            discountAmount = getTotalDiscountPrice(),\n            paymentAmount = getTotalPaymentPrice(),\n            supplyAmount = getTotalSupplyPrice(),\n        )\n    }\n\n    fun confirmPayment(approvedAt: LocalDateTime, pgPaymentInfo: PgPaymentInfo, orderValidator: OrderValidator) {\n        issueDoneOrderEvent()\n        orderValidator.validCanConfirmPayment(this)\n        orderStatus = OrderStatus.CONFIRM\n        this.approvedAt = approvedAt\n        this.pgPaymentInfo = pgPaymentInfo\n    }\n\n    fun approve(orderValidator: OrderValidator) {\n        issueDoneOrderEvent()\n        orderValidator.validCanApproveOrder(this)\n        approvedAt = LocalDateTime.now()\n        orderStatus = OrderStatus.APPROVED\n    }\n\n    fun freeConfirm(currentUserId: Long, orderValidator: OrderValidator) {\n        orderValidator.validOwner(this, currentUserId)\n        issueDoneOrderEvent()\n        orderValidator.validCanFreeConfirm(this)\n        approvedAt = LocalDateTime.now()\n        orderStatus = OrderStatus.APPROVED\n    }\n\n    private fun issueDoneOrderEvent() {\n        if (orderStatus.isCanDone()) {\n            Events.raise(DoneOrderEvent.from(this))\n        }\n    }\n\n    fun cancel(orderValidator: OrderValidator, reason: String? = null) {\n        orderValidator.validCanCancel(this)\n        orderStatus = OrderStatus.CANCELED\n        cancelReason = reason?.take(500)\n        refundStatus = RefundStatus.REFUND_REQUESTED\n        refundStatusChangedAt = LocalDateTime.now()\n        withDrawAt = LocalDateTime.now()\n        Events.raise(WithDrawOrderEvent.from(this))\n    }\n\n    fun refuse(orderValidator: OrderValidator, reason: String? = null) {\n        orderValidator.validCanRefuse(this)\n        orderStatus = OrderStatus.CANCELED\n        cancelReason = reason?.take(500)\n        refundStatus = RefundStatus.REFUND_REQUESTED\n        refundStatusChangedAt = LocalDateTime.now()\n        withDrawAt = LocalDateTime.now()\n        Events.raise(WithDrawOrderEvent.from(this))\n    }\n\n    fun refund(currentUserId: Long, orderValidator: OrderValidator, reason: String? = null) {\n        orderValidator.validOwner(this, currentUserId)\n        orderValidator.validCanRefund(this)\n        orderStatus = OrderStatus.REFUND\n        cancelReason = reason?.take(500)\n        refundStatus = RefundStatus.REFUND_REQUESTED\n        refundStatusChangedAt = LocalDateTime.now()\n        withDrawAt = LocalDateTime.now()\n        Events.raise(WithDrawOrderEvent.from(this))\n    }\n\n    fun fail(reason: String? = null) {\n        orderStatus = OrderStatus.FAILED\n        failReason = reason?.take(500)\n    }\n\n    fun completeRefund() {\n        refundStatus = RefundStatus.REFUND_COMPLETED\n        refundStatusChangedAt = LocalDateTime.now()\n    }\n\n    fun attachCoupon(orderCouponVo: OrderCouponVo) {\n        this.orderCouponVo = orderCouponVo\n    }\n\n    val paymentKey: String\n        get() = pgPaymentInfo.paymentKey.takeIf { it.isNotEmpty() }\n            ?: throw NotPaymentOrderException.EXCEPTION\n\n    fun getCouponName(): String = orderCouponVo.name\n\n    fun getTotalSupplyPrice(): Money =\n        orderLineItems.fold(Money.ZERO) { acc, item -> acc.plus(item.getTotalOrderLinePrice()) }\n\n    fun getTotalPaymentPrice(): Money = getTotalSupplyPrice().minus(getTotalDiscountPrice())\n\n    fun getTotalDiscountPrice(): Money = orderCouponVo.discountAmount\n\n    fun hasCoupon(): Boolean = !orderCouponVo.isDefault()\n\n    private fun getOrderLineItem(): OrderLineItem =\n        orderLineItems.firstOrNull() ?: throw OrderLineNotFountException.EXCEPTION\n\n    val itemId: Long\n        get() = getOrderLineItem().getItemId()\n\n    fun getItemGroupId(): Long = getOrderLineItem().getItemGroupId()\n\n    fun isNeedPaid(): Boolean =\n        Money.ZERO.isLessThan(getTotalPaymentPrice()) && orderMethod!!.isPayment()\n\n    fun getMethod(): String {\n        if (orderMethod == OrderMethod.APPROVAL) return OrderMethod.APPROVAL.kr\n        return pgPaymentInfo.paymentMethod.kr\n    }\n\n    fun getProvider(): String = pgPaymentInfo.paymentProvider\n\n    fun getReceiptUrl(): String = pgPaymentInfo.receiptUrl\n\n    fun isPaid(): Boolean = isNeedPaid()\n\n    fun isDudoongTicketOrder(): Boolean =\n        getTotalPaymentPrice().isGreaterThan(Money.ZERO) && orderMethod == OrderMethod.APPROVAL\n\n    fun getDistinctItemIds(): List<Long> =\n        orderLineItems.map { it.getItemId() }.distinct()\n\n    fun getTotalQuantity(): Long =\n        orderLineItems.sumOf { it.quantity!! }\n\n    fun toEmailOrderInfo(): EmailOrderInfo =\n        EmailOrderInfo(orderName!!, getTotalQuantity(), getTotalPaymentPrice().toString(), createdAtKt())\n\n    fun toAlimTalkOrderInfo(): AlimTalkOrderInfo =\n        AlimTalkOrderInfo(orderName!!, getTotalQuantity(), getTotalPaymentPrice().toString(), createdAtKt())\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/domain/OrderCouponVo.kt",
    "content": "package band.gosrock.domain.domains.order.domain\n\nimport band.gosrock.common.consts.DuDoongStatic.MINIMUM_PAYMENT_WON\nimport band.gosrock.common.consts.DuDoongStatic.ZERO\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.coupon.domain.IssuedCoupon\nimport band.gosrock.domain.domains.order.exception.LessThanMinmumPaymentOrderException\nimport jakarta.persistence.Column\nimport jakarta.persistence.Embeddable\n\n@Embeddable\nclass OrderCouponVo() {\n\n    @Column(name = \"coupon_name\")\n    var name: String = \"사용하지 않음\"\n        protected set\n\n    var discountAmount: Money = Money.ZERO\n        protected set\n\n    var couponId: Long = ZERO\n        protected set\n\n    companion object {\n        @JvmStatic\n        fun of(coupon: IssuedCoupon, orderSupplyAmount: Money): OrderCouponVo =\n            OrderCouponVo().apply {\n                couponId = coupon.getIssuedCouponId()!!\n                discountAmount = coupon.getDiscountAmount(orderSupplyAmount)\n                name = coupon.getCouponName()!!\n            }\n\n        @JvmStatic\n        fun empty(): OrderCouponVo = OrderCouponVo()\n    }\n\n    fun validMinimumPaymentAmount(supplyAmount: Money) {\n        val paymentAmount = supplyAmount.minus(discountAmount)\n        if (paymentAmount != Money.ZERO && paymentAmount.isLessThan(Money.wons(MINIMUM_PAYMENT_WON))) {\n            throw LessThanMinmumPaymentOrderException.EXCEPTION\n        }\n    }\n\n    fun isDefault(): Boolean = couponId == ZERO\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/domain/OrderItemVo.kt",
    "content": "package band.gosrock.domain.domains.order.domain\n\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport jakarta.persistence.Embeddable\n\n@Embeddable\nclass OrderItemVo() {\n\n    var name: String? = null\n        protected set\n\n    var price: Money? = null\n        protected set\n\n    var itemGroupId: Long? = null\n        protected set\n\n    var itemId: Long? = null\n        protected set\n\n    companion object {\n        @JvmStatic\n        fun from(ticketItem: TicketItem): OrderItemVo = OrderItemVo().apply {\n            itemGroupId = ticketItem.eventId\n            itemId = ticketItem.id\n            price = ticketItem.price\n            name = ticketItem.name\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/domain/OrderLineItem.kt",
    "content": "package band.gosrock.domain.domains.order.domain\n\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.cart.domain.CartLineItem\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport jakarta.persistence.CascadeType\nimport jakarta.persistence.Column\nimport jakarta.persistence.Embedded\nimport jakarta.persistence.Entity\nimport jakarta.persistence.FetchType\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\nimport jakarta.persistence.JoinColumn\nimport jakarta.persistence.OneToMany\n\n@Entity(name = \"tbl_order_line\")\nclass OrderLineItem() : BaseTimeEntity() {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"order_line_item_id\")\n    var id: Long? = null\n        protected set\n\n    @Embedded\n    var orderItem: OrderItemVo? = null\n        protected set\n\n    var quantity: Long? = null\n        protected set\n\n    @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER)\n    @JoinColumn(name = \"order_line_item_id\")\n    var orderOptionAnswers: MutableList<OrderOptionAnswer> = mutableListOf()\n        protected set\n\n    companion object {\n        @JvmStatic\n        fun of(cartLineItem: CartLineItem, ticketItem: TicketItem): OrderLineItem {\n            val orderOptionAnswers = cartLineItem.cartOptionAnswers.map { OrderOptionAnswer.from(it) }\n            return OrderLineItem().apply {\n                this.orderOptionAnswers.addAll(orderOptionAnswers)\n                this.quantity = cartLineItem.quantity\n                this.orderItem = OrderItemVo.from(ticketItem)\n            }\n        }\n\n        /** 테스트 전용 팩토리 메서드 */\n        @JvmStatic\n        fun forTest(\n            orderOptionAnswer: List<OrderOptionAnswer> = emptyList(),\n            quantity: Long? = null,\n            orderItemVo: OrderItemVo? = null,\n        ): OrderLineItem = OrderLineItem().apply {\n            this.orderOptionAnswers.addAll(orderOptionAnswer)\n            this.quantity = quantity\n            this.orderItem = orderItemVo\n        }\n    }\n\n    private val safeOrderItem: OrderItemVo\n        get() = orderItem ?: throw IllegalStateException(\"OrderItem is not initialized\")\n\n    private val safeQuantity: Long\n        get() = quantity ?: throw IllegalStateException(\"Quantity is not initialized\")\n\n    fun getOptionAnswersPrice(): Money =\n        orderOptionAnswers.fold(Money.ZERO) { acc, answer -> acc.plus(answer.additionalPrice) }\n\n    fun getTotalOrderLinePrice(): Money =\n        getItemPrice().plus(getOptionAnswersPrice()).times(safeQuantity.toDouble())\n\n    fun getItemPrice(): Money = safeOrderItem.price ?: throw IllegalStateException(\"OrderItem price is not set\")\n\n    fun isNeedPaid(): Boolean = Money.ZERO.isLessThan(getTotalOrderLinePrice())\n\n    fun getItemId(): Long = safeOrderItem.itemId ?: throw IllegalStateException(\"OrderItem itemId is not set\")\n\n    fun getItemGroupId(): Long = safeOrderItem.itemGroupId ?: throw IllegalStateException(\"OrderItem itemGroupId is not set\")\n\n    fun getItemName(): String = safeOrderItem.name ?: throw IllegalStateException(\"OrderItem name is not set\")\n\n    fun getAnswerOptionIds(): List<Long> =\n        orderOptionAnswers.map { it.optionId ?: throw IllegalStateException(\"OrderOptionAnswer optionId is not set\") }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/domain/OrderMethod.kt",
    "content": "package band.gosrock.domain.domains.order.domain\n\nimport com.fasterxml.jackson.annotation.JsonValue\n\nenum class OrderMethod(\n    val value: String,\n    @JsonValue val kr: String,\n) {\n    APPROVAL(\"APPROVAL\", \"승인 방식\"),\n    PAYMENT(\"PAYMENT\", \"결제 방식\");\n\n    fun isPayment(): Boolean = this == PAYMENT\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/domain/OrderOptionAnswer.kt",
    "content": "package band.gosrock.domain.domains.order.domain\n\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.common.vo.OptionAnswerVo\nimport band.gosrock.domain.domains.cart.domain.CartOptionAnswer\nimport band.gosrock.domain.domains.ticket_item.domain.Option\nimport jakarta.persistence.Column\nimport jakarta.persistence.Entity\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\n\n@Entity(name = \"tbl_order_option_answer\")\nclass OrderOptionAnswer() : BaseTimeEntity() {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"order_option_answer_id\")\n    var id: Long? = null\n        protected set\n\n    var optionId: Long? = null\n        protected set\n\n    var additionalPrice: Money = Money.ZERO\n        protected set\n\n    var answer: String? = null\n        protected set\n\n    companion object {\n        @JvmStatic\n        fun from(cartOptionAnswer: CartOptionAnswer): OrderOptionAnswer = OrderOptionAnswer().apply {\n            answer = cartOptionAnswer.answer\n            optionId = cartOptionAnswer.optionId\n            additionalPrice = cartOptionAnswer.additionalPrice\n        }\n    }\n\n    fun getOptionAnswerVo(option: Option): OptionAnswerVo =\n        OptionAnswerVo(\n            questionDescription = option.getQuestionDescription(),\n            answer = answer,\n            optionGroupType = option.getQuestionType(),\n            questionName = option.getQuestionName(),\n            additionalPrice = additionalPrice,\n        )\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/domain/OrderStatus.kt",
    "content": "package band.gosrock.domain.domains.order.domain\n\nimport com.fasterxml.jackson.annotation.JsonValue\n\nenum class OrderStatus(\n    val value: String,\n    @JsonValue val kr: String,\n) {\n    READY(\"READY\", \"주문 생성상태\"),\n    PENDING_PAYMENT(\"PENDING_PAYMENT\", \"결제 대기중\"),\n    PENDING_APPROVE(\"PENDING_APPROVE\", \"승인 대기중\"),\n    OUTDATED(\"OUTDATED\", \"결제 시간 만료\"),\n    CONFIRM(\"CONFIRM\", \"결제 완료\"),\n    APPROVED(\"APPROVED\", \"승인 완료\"),\n    REFUND(\"REFUND\", \"환불 완료\"),\n    CANCELED(\"CANCELED\", \"취소된 결제\"),\n    FAILED(\"FAILED\", \"결제 실패\");\n\n    fun isInEventOrderExcelStatus(): Boolean =\n        this == CONFIRM || this == CANCELED || this == APPROVED || this == REFUND\n\n    fun isCanDone(): Boolean =\n        this == PENDING_PAYMENT || this == PENDING_APPROVE\n\n    fun isCanWithDraw(): Boolean =\n        this == APPROVED || this == CONFIRM\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/domain/PaymentInfo.kt",
    "content": "package band.gosrock.domain.domains.order.domain\n\nimport band.gosrock.domain.common.vo.Money\nimport jakarta.persistence.AttributeOverride\nimport jakarta.persistence.Column\nimport jakarta.persistence.Embeddable\nimport jakarta.persistence.Embedded\n\n@Embeddable\nclass PaymentInfo() {\n\n    @Embedded\n    @AttributeOverride(name = \"amount\", column = Column(name = \"payment_amount\"))\n    var paymentAmount: Money? = null\n        protected set\n\n    @Embedded\n    @AttributeOverride(name = \"amount\", column = Column(name = \"supply_amount\"))\n    var supplyAmount: Money? = null\n        protected set\n\n    @Embedded\n    @AttributeOverride(name = \"amount\", column = Column(name = \"discount_amount\"))\n    var discountAmount: Money? = null\n        protected set\n\n    companion object {\n        @JvmStatic\n        fun of(paymentAmount: Money, supplyAmount: Money, discountAmount: Money): PaymentInfo =\n            PaymentInfo().apply {\n                this.paymentAmount = paymentAmount\n                this.supplyAmount = supplyAmount\n                this.discountAmount = discountAmount\n            }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/domain/PaymentMethod.kt",
    "content": "package band.gosrock.domain.domains.order.domain\n\nimport band.gosrock.domain.domains.order.exception.NotSupportedOrderMethodException\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.response.TossPaymentMethod\nimport com.fasterxml.jackson.annotation.JsonValue\n\nenum class PaymentMethod(\n    val value: String,\n    @JsonValue val kr: String,\n) {\n    EASYPAY(\"EASYPAY\", \"간편 결제\"),\n    CARD(\"CARD\", \"카드 결제\"),\n    DEFAULT(\"DEFAULT\", \"\");\n\n    companion object {\n        @JvmStatic\n        fun from(tossPaymentMethod: TossPaymentMethod): PaymentMethod =\n            try {\n                valueOf(tossPaymentMethod.name)\n            } catch (e: IllegalArgumentException) {\n                throw NotSupportedOrderMethodException.EXCEPTION\n            }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/domain/PgPaymentInfo.kt",
    "content": "package band.gosrock.domain.domains.order.domain\n\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.response.PaymentsResponse\nimport jakarta.persistence.AttributeOverride\nimport jakarta.persistence.Column\nimport jakarta.persistence.Embeddable\nimport jakarta.persistence.Embedded\nimport jakarta.persistence.EnumType\nimport jakarta.persistence.Enumerated\n\n@Embeddable\nclass PgPaymentInfo() {\n\n    @Enumerated(EnumType.STRING)\n    var paymentMethod: PaymentMethod = PaymentMethod.DEFAULT\n        protected set\n\n    var paymentProvider: String = \"\"\n        protected set\n\n    var receiptUrl: String = \"\"\n        protected set\n\n    var paymentKey: String = \"\"\n        protected set\n\n    @Embedded\n    @AttributeOverride(name = \"amount\", column = Column(name = \"vat_amount\"))\n    var vat: Money = Money.ZERO\n        protected set\n\n    companion object {\n        @JvmStatic\n        fun from(paymentsResponse: PaymentsResponse): PgPaymentInfo = PgPaymentInfo().apply {\n            paymentKey = paymentsResponse.paymentKey!!\n            paymentMethod = PaymentMethod.from(paymentsResponse.method!!)\n            paymentProvider = paymentsResponse.getProviderName()\n            receiptUrl = paymentsResponse.receipt!!.url!!\n            vat = Money.wons(paymentsResponse.vat!!)\n        }\n\n        @JvmStatic\n        fun empty(): PgPaymentInfo = PgPaymentInfo()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/domain/RefundStatus.kt",
    "content": "package band.gosrock.domain.domains.order.domain\n\nenum class RefundStatus(val value: String, val description: String) {\n    NONE(\"NONE\", \"환불 불필요\"),\n    REFUND_REQUESTED(\"REFUND_REQUESTED\", \"환불 요청됨\"),\n    REFUND_COMPLETED(\"REFUND_COMPLETED\", \"환불 완료\"),\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/domain/validator/OrderValidator.kt",
    "content": "package band.gosrock.domain.domains.order.domain.validator\n\nimport band.gosrock.common.annotation.Validator\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.issuedTicket.adaptor.IssuedTicketAdaptor\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderLineItem\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport band.gosrock.domain.domains.order.exception.ApproveWaitingOrderPurchaseLimitException\nimport band.gosrock.domain.domains.order.exception.CanNotApproveDeletedUserOrderException\nimport band.gosrock.domain.domains.order.exception.CanNotCancelOrderException\nimport band.gosrock.domain.domains.order.exception.CanNotRefundOrderException\nimport band.gosrock.domain.domains.order.exception.InvalidOrderException\nimport band.gosrock.domain.domains.order.exception.NotApprovalOrderException\nimport band.gosrock.domain.domains.order.exception.NotFreeOrderException\nimport band.gosrock.domain.domains.order.exception.NotOwnerOrderException\nimport band.gosrock.domain.domains.order.exception.NotPaymentOrderException\nimport band.gosrock.domain.domains.order.exception.NotPendingOrderException\nimport band.gosrock.domain.domains.order.exception.NotRefundAvailableDateOrderException\nimport band.gosrock.domain.domains.order.exception.OrdeItemNotOneTypeException\nimport band.gosrock.domain.domains.order.exception.OrderItemOptionChangedException\nimport band.gosrock.domain.domains.ticket_item.adaptor.OptionAdaptor\nimport band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor\nimport band.gosrock.domain.domains.ticket_item.domain.Option\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport java.util.Objects\n\n@Validator\nclass OrderValidator(\n    private val eventAdaptor: EventAdaptor,\n    private val itemAdaptor: TicketItemAdaptor,\n    private val issuedTicketAdaptor: IssuedTicketAdaptor,\n    private val optionAdaptor: OptionAdaptor,\n    private val userAdaptor: UserAdaptor,\n    private val orderAdaptor: OrderAdaptor,\n) {\n    fun validCanCreate(order: Order) {\n        val item = getItem(order)\n        val event = getEvent(order)\n        validEventIsOpen(event)\n        validTicketingTime(event)\n        validItemStockEnough(order, item)\n        validItemKindIsOneType(order)\n        validItemPurchaseLimit(order, item)\n        validOptionNotChange(order, item)\n    }\n\n    fun validOptionNotChange(order: Order, item: TicketItem) {\n        val orderLineItems = order.orderLineItems\n        val itemsOptionGroupIds = item.getOptionGroupIds()\n        orderLineItems.forEach { orderLineItem ->\n            if (!Objects.equals(getAnswerOptionGroupIds(orderLineItem), itemsOptionGroupIds)) {\n                throw OrderItemOptionChangedException.EXCEPTION\n            }\n        }\n    }\n\n    fun validCanApproveOrder(order: Order) {\n        validMethodIsCanApprove(order)\n        validStatusCanApprove(order.orderStatus)\n        validCanDone(order)\n        validUserNotDeleted(order)\n    }\n\n    fun validUserNotDeleted(order: Order) {\n        val user = userAdaptor.queryUser(order.userId!!)\n        if (user.isDeletedUser()) {\n            throw CanNotApproveDeletedUserOrderException.EXCEPTION\n        }\n    }\n\n    fun validCanConfirmPayment(order: Order) {\n        validMethodIsPaymentOrder(order)\n        validStatusCanPaymentConfirm(order.orderStatus)\n        validCanDone(order)\n    }\n\n    fun validCanFreeConfirm(order: Order) {\n        validAmountIsFree(order)\n        validStatusCanPaymentConfirm(order.orderStatus)\n        validCanDone(order)\n    }\n\n    fun validCanCancel(order: Order) {\n        validAvailableRefundDate(order)\n        validStatusCanCancel(order.orderStatus)\n        validCanWithDraw(order)\n    }\n\n    fun validCanRefuse(order: Order) {\n        validAvailableRefundDate(order)\n        validStatusCanRefuse(order.orderStatus)\n        validCanWithDraw(order)\n    }\n\n    fun validCanRefund(order: Order) {\n        validAvailableRefundDate(order)\n        validStatusCanRefund(order.orderStatus)\n        validCanWithDraw(order)\n    }\n\n    fun validCanDone(order: Order) {\n        val item = getItem(order)\n        val event = getEvent(order)\n        validEventIsOpen(event)\n        validTicketingTime(event)\n        validItemStockEnough(order, item)\n        validItemPurchaseLimit(order, item)\n        validOptionNotChange(order, item)\n    }\n\n    fun validCanWithDraw(order: Order) {\n        val event = getEvent(order)\n        validEventIsOpen(event)\n        validTicketingTime(event)\n    }\n\n    fun validItemPurchaseLimit(order: Order, item: TicketItem) {\n        val paidTicketCount = issuedTicketAdaptor.countPaidTicket(order.userId!!, item.id!!)\n        val totalIssuedCount = paidTicketCount + order.getTotalQuantity()\n        item.validPurchaseLimit(totalIssuedCount)\n    }\n\n    fun validApproveStatePurchaseLimit(order: Order) {\n        val item = getItem(order)\n        val userId = order.userId!!\n        val paidTicketCount = issuedTicketAdaptor.countPaidTicket(userId, item.id!!)\n        val approveWaitingOrders = orderAdaptor.findByEventIdAndOrderStatusAndUserId(\n            order.eventId!!, userId, OrderStatus.PENDING_APPROVE\n        )\n        val approveWaitingTicketCount = approveWaitingOrders\n            .filter { Objects.equals(item.id, it.itemId) }\n            .sumOf { it.getTotalQuantity() }\n        val totalIssuedCount = paidTicketCount + approveWaitingTicketCount + order.getTotalQuantity()\n        if (item.isPurchaseLimitExceed(totalIssuedCount)) {\n            throw ApproveWaitingOrderPurchaseLimitException.EXCEPTION\n        }\n    }\n\n    fun validApproveOrderCreateTotalStock(order: Order) {\n        val item = getItem(order)\n        val approveWaitingOrders = orderAdaptor.findByEventIdAndOrderStatus(\n            order.eventId!!, OrderStatus.PENDING_APPROVE\n        )\n        val approveWaitingTicketCount = approveWaitingOrders.sumOf { it.getTotalQuantity() }\n        val expectedApproveWaitQuantity = approveWaitingTicketCount + order.getTotalQuantity()\n        item.validEnoughQuantity(expectedApproveWaitQuantity)\n    }\n\n    fun validEventIsOpen(event: Event) {\n        event.validateNotOpenStatus()\n    }\n\n    fun validItemKindIsOneType(order: Order) {\n        val itemIds = order.getDistinctItemIds()\n        if (itemIds.size != 1) {\n            throw OrdeItemNotOneTypeException.EXCEPTION\n        }\n    }\n\n    fun validTicketingTime(event: Event) {\n        event.validateTicketingTime()\n    }\n\n    fun validItemStockEnough(order: Order, item: TicketItem) {\n        item.validEnoughQuantity(order.getTotalQuantity())\n    }\n\n    fun validMethodIsCanApprove(order: Order) {\n        if (isMethodPayment(order)) {\n            throw NotApprovalOrderException.EXCEPTION\n        }\n    }\n\n    fun validOwner(order: Order, currentUserId: Long) {\n        if (order.userId != currentUserId) {\n            throw NotOwnerOrderException.EXCEPTION\n        }\n    }\n\n    fun validAmountIsSameAsRequest(order: Order, requestAmount: Money) {\n        if (order.getTotalPaymentPrice() != requestAmount) {\n            throw InvalidOrderException.EXCEPTION\n        }\n    }\n\n    fun validMethodIsPaymentOrder(order: Order) {\n        if (!isMethodPayment(order)) {\n            throw NotPaymentOrderException.EXCEPTION\n        }\n    }\n\n    fun validAvailableRefundDate(order: Order) {\n        if (!isRefundDateNotPassed(order)) {\n            throw NotRefundAvailableDateOrderException.EXCEPTION\n        }\n    }\n\n    fun validAmountIsFree(order: Order) {\n        if (order.isNeedPaid()) {\n            throw NotFreeOrderException.EXCEPTION\n        }\n    }\n\n    fun isRefundDateNotPassed(order: Order): Boolean {\n        val events = eventAdaptor.findAllByIds(getEventIds(order))\n        return events.map { it.isRefundDateNotPassed() }.reduce { a, b -> a && b }\n    }\n\n    private fun getEventIds(order: Order): List<Long> =\n        order.orderLineItems.map { it.orderItem!!.itemGroupId!! }\n\n    private fun isMethodPayment(order: Order): Boolean = order.orderMethod!!.isPayment()\n\n    fun isStatusCanWithDraw(orderStatus: OrderStatus): Boolean =\n        orderStatus == OrderStatus.CONFIRM || orderStatus == OrderStatus.APPROVED\n\n    fun isStatusCanRefuse(orderStatus: OrderStatus): Boolean =\n        orderStatus == OrderStatus.PENDING_APPROVE\n\n    fun validStatusCanCancel(orderStatus: OrderStatus) {\n        if (!isStatusCanWithDraw(orderStatus)) throw CanNotCancelOrderException.EXCEPTION\n    }\n\n    fun validStatusCanRefund(orderStatus: OrderStatus) {\n        if (!isStatusCanWithDraw(orderStatus)) throw CanNotRefundOrderException.EXCEPTION\n    }\n\n    fun validStatusCanRefuse(orderStatus: OrderStatus) {\n        if (!isStatusCanRefuse(orderStatus)) throw CanNotCancelOrderException.EXCEPTION\n    }\n\n    fun validStatusCanPaymentConfirm(orderStatus: OrderStatus) {\n        if (orderStatus != OrderStatus.PENDING_PAYMENT) throw NotPendingOrderException.EXCEPTION\n    }\n\n    fun validStatusCanApprove(orderStatus: OrderStatus) {\n        if (orderStatus != OrderStatus.PENDING_APPROVE) throw NotPendingOrderException.EXCEPTION\n    }\n\n    private fun getEvent(order: Order): Event = eventAdaptor.findById(order.getItemGroupId())\n\n    private fun getItem(order: Order): TicketItem = itemAdaptor.queryTicketItem(order.itemId)\n\n    private fun getAnswerOptionGroupIds(orderLineItem: OrderLineItem): List<Long> {\n        val answerOptions = getOptionsFrom(orderLineItem)\n        return answerOptions.map { it.getOptionGroupId()!! }.sorted()\n    }\n\n    private fun getOptionsFrom(orderLineItem: OrderLineItem): List<Option> =\n        optionAdaptor.findAllByIds(orderLineItem.getAnswerOptionIds())\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/ApproveWaitingOrderPurchaseLimitException.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass ApproveWaitingOrderPurchaseLimitException private constructor() : DuDoongCodeException(OrderErrorCode.APPROVE_WAITING_PURCHASE_LIMIT) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = ApproveWaitingOrderPurchaseLimitException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/CanNotApproveDeletedUserOrderException.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CanNotApproveDeletedUserOrderException private constructor() : DuDoongCodeException(OrderErrorCode.CAN_NOT_DELETED_USER_APPROVE) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CanNotApproveDeletedUserOrderException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/CanNotCancelOrderException.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CanNotCancelOrderException private constructor() : DuDoongCodeException(OrderErrorCode.ORDER_CANNOT_CANCEL) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CanNotCancelOrderException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/CanNotRefundOrderException.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CanNotRefundOrderException private constructor() : DuDoongCodeException(OrderErrorCode.ORDER_CANNOT_REFUND) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CanNotRefundOrderException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/CanNotRefuseOrderException.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass CanNotRefuseOrderException private constructor() : DuDoongCodeException(OrderErrorCode.ORDER_CANNOT_REFUSE) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = CanNotRefuseOrderException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/InvalidOrderException.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass InvalidOrderException private constructor() : DuDoongCodeException(OrderErrorCode.ORDER_NOT_VALID) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = InvalidOrderException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/LessThanMinmumPaymentOrderException.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass LessThanMinmumPaymentOrderException private constructor() : DuDoongCodeException(OrderErrorCode.ORDER_LESS_THAN_MINIMUM) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = LessThanMinmumPaymentOrderException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/NotApprovalOrderException.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass NotApprovalOrderException private constructor() : DuDoongCodeException(OrderErrorCode.ORDER_NOT_APPROVAL) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = NotApprovalOrderException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/NotFreeOrderException.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass NotFreeOrderException private constructor() : DuDoongCodeException(OrderErrorCode.ORDER_NOT_FREE) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = NotFreeOrderException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/NotOwnerOrderException.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass NotOwnerOrderException private constructor() : DuDoongCodeException(OrderErrorCode.ORDER_NOT_MINE) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = NotOwnerOrderException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/NotPaymentOrderException.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass NotPaymentOrderException private constructor() : DuDoongCodeException(OrderErrorCode.ORDER_NOT_PAYMENT) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = NotPaymentOrderException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/NotPendingOrderException.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass NotPendingOrderException private constructor() : DuDoongCodeException(OrderErrorCode.ORDER_NOT_PENDING) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = NotPendingOrderException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/NotRefundAvailableDateOrderException.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass NotRefundAvailableDateOrderException private constructor() : DuDoongCodeException(OrderErrorCode.ORDER_NOT_REFUND_DATE) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = NotRefundAvailableDateOrderException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/NotSupportedOrderMethodException.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass NotSupportedOrderMethodException private constructor() : DuDoongCodeException(OrderErrorCode.ORDER_NOT_SUPPORTED_METHOD) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = NotSupportedOrderMethodException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/OrdeItemNotOneTypeException.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass OrdeItemNotOneTypeException private constructor() : DuDoongCodeException(OrderErrorCode.ORDER_INVALID_ITEM_KIND_POLICY) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = OrdeItemNotOneTypeException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/OrderErrorCode.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.consts.DuDoongStatic.BAD_REQUEST\nimport band.gosrock.common.consts.DuDoongStatic.NOT_FOUND\nimport band.gosrock.common.dto.ErrorReason\nimport band.gosrock.common.exception.BaseErrorCode\nimport java.lang.reflect.Field\n\nenum class OrderErrorCode(\n    private val status: Int,\n    private val code: String,\n    private val reason: String,\n) : BaseErrorCode {\n    @ExplainError(\"본인의 주문이 아닐 때 발생하는 오류. 본인의 주문만 상태변경이 가능한 api 들이 있습니다.\")\n    ORDER_NOT_MINE(BAD_REQUEST, \"Order_400_1\", \"본인의 주문이 아닙니다.\"),\n\n    @ExplainError(\"토스 결제 금액과 , 주문금액이 다를 때 등 올바르지 않은 주문 상태를 가질 때 발생하는 오류입니다.\")\n    ORDER_NOT_VALID(BAD_REQUEST, \"Order_400_2\", \"올바르지 않은 주문입니다.\"),\n    ORDER_NOT_PENDING(BAD_REQUEST, \"Order_400_3\", \"결제,승인 대기중인 주문이 아닙니다.\"),\n    ORDER_NOT_SUPPORTED_METHOD(BAD_REQUEST, \"Order_400_4\", \"지원하지 않는 방식의 주문입니다.\"),\n    ORDER_CANNOT_CANCEL(BAD_REQUEST, \"Order_400_5\", \"주문을 취소할 수 없는 상태입니다.\"),\n    ORDER_CANNOT_REFUND(BAD_REQUEST, \"Order_400_6\", \"주문을 환불할 수 없는 상태입니다.\"),\n    ORDER_NOT_APPROVAL(BAD_REQUEST, \"Order_400_7\", \"승인 주문이 아닙니다.\"),\n    ORDER_NOT_PAYMENT(BAD_REQUEST, \"Order_400_8\", \"결제 주문이 아닙니다.\"),\n    ORDER_NOT_REFUND_DATE(BAD_REQUEST, \"Order_400_9\", \"환불을 할 수 있는 기한을 지났습니다.\"),\n    ORDER_NOT_FOUND(NOT_FOUND, \"Order_404_1\", \"주문을 찾을 수 없습니다.\"),\n    ORDER_LINE_NOT_FOUND(NOT_FOUND, \"Order_404_2\", \"주문 라인을 찾을 수 없습니다.\"),\n    ORDER_NOT_FREE(BAD_REQUEST, \"Order_400_10\", \"무료 주문이 아닙니다.\"),\n    ORDER_LESS_THAN_MINIMUM(BAD_REQUEST, \"Order_400_11\", \"최소 결제금액인 1000원보다 낮은 주문입니다.\"),\n\n    @ExplainError(\"한 장바구니엔 관련된 한 아이템만 올수 있음\")\n    ORDER_INVALID_ITEM_KIND_POLICY(BAD_REQUEST, \"Order_400_12\", \"장바구니에 아이템을 담는 정책을 위반하였습니다.\"),\n    ORDER_OPTION_CHANGED(BAD_REQUEST, \"Order_400_13\", \"주문 과정중 아이템의 옵션이 변화했습니다.\"),\n    CAN_NOT_DELETED_USER_APPROVE(BAD_REQUEST, \"Order_400_14\", \"유저가 탈퇴를 했습니다.\"),\n    APPROVE_WAITING_PURCHASE_LIMIT(BAD_REQUEST, \"Order_400_15\", \"승인 대기중인 주문으로 인해 티켓 최대 구매 가능 횟수를 넘겼습니다.이미 신청한 주문이 승인 될 때까지 기다려주세요.\"),\n    ORDER_CANNOT_REFUSE(BAD_REQUEST, \"Order_400_16\", \"승인 대기중인 주문을 거절할 수 없는 상태입니다.\");\n\n    override fun getErrorReason(): ErrorReason =\n        ErrorReason(status = status, code = code, reason = reason)\n\n    override fun getExplainError(): String {\n        val field: Field = this.javaClass.getField(this.name)\n        val annotation: ExplainError? = field.getAnnotation(ExplainError::class.java)\n        return annotation?.value ?: this.reason\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/OrderItemOptionChangedException.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass OrderItemOptionChangedException private constructor() : DuDoongCodeException(OrderErrorCode.ORDER_OPTION_CHANGED) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = OrderItemOptionChangedException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/OrderLineNotFountException.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass OrderLineNotFountException private constructor() : DuDoongCodeException(OrderErrorCode.ORDER_LINE_NOT_FOUND) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = OrderLineNotFountException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/exception/OrderNotFoundException.kt",
    "content": "package band.gosrock.domain.domains.order.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass OrderNotFoundException private constructor() : DuDoongCodeException(OrderErrorCode.ORDER_NOT_FOUND) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = OrderNotFoundException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/repository/OrderCustomRepository.kt",
    "content": "package band.gosrock.domain.domains.order.repository\n\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.RefundStatus\nimport band.gosrock.domain.domains.order.repository.condition.FindEventOrdersCondition\nimport band.gosrock.domain.domains.order.repository.condition.FindMyPageOrderCondition\nimport java.util.Optional\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.domain.Slice\n\ninterface OrderCustomRepository {\n    fun findByOrderUuid(orderUuid: String): Optional<Order>\n    fun findMyOrders(condition: FindMyPageOrderCondition, pageable: Pageable): Slice<Order>\n    fun findEventOrders(condition: FindEventOrdersCondition, pageable: Pageable): Page<Order>\n    fun findRecentOrder(userId: Long): Optional<Order>\n    fun findRefunds(eventId: Long?, refundStatus: RefundStatus?, keyword: String?, pageable: Pageable): Page<Order>\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/repository/OrderCustomRepositoryImpl.kt",
    "content": "package band.gosrock.domain.domains.order.repository\n\nimport band.gosrock.domain.common.util.SliceUtil\nimport band.gosrock.domain.domains.event.domain.QEvent.event\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport band.gosrock.domain.domains.order.domain.QOrder.order\nimport band.gosrock.domain.domains.order.domain.RefundStatus\nimport band.gosrock.domain.domains.order.domain.QOrderLineItem.orderLineItem\nimport band.gosrock.domain.domains.order.repository.condition.FindEventOrdersCondition\nimport band.gosrock.domain.domains.order.repository.condition.FindMyPageOrderCondition\nimport band.gosrock.domain.domains.user.domain.AccountState\nimport band.gosrock.domain.domains.user.domain.QUser.user\nimport com.querydsl.core.types.dsl.BooleanExpression\nimport com.querydsl.core.types.dsl.DateTemplate\nimport com.querydsl.core.types.dsl.Expressions\nimport com.querydsl.jpa.impl.JPAQuery\nimport com.querydsl.jpa.impl.JPAQueryFactory\nimport java.time.LocalDateTime\nimport java.util.Optional\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.domain.Slice\nimport org.springframework.data.support.PageableExecutionUtils\n\nclass OrderCustomRepositoryImpl(\n    private val queryFactory: JPAQueryFactory\n) : OrderCustomRepository {\n\n    override fun findByOrderUuid(orderUuid: String): Optional<Order> {\n        val find = queryFactory\n            .selectFrom(order)\n            .leftJoin(order.orderLineItems, orderLineItem)\n            .fetchJoin()\n            .where(order.uuid.eq(orderUuid))\n            .fetchOne()\n        return Optional.ofNullable(find)\n    }\n\n    override fun findMyOrders(condition: FindMyPageOrderCondition, pageable: Pageable): Slice<Order> {\n        val orders = queryFactory\n            .selectFrom(order)\n            .join(event)\n            .on(order.eventId.eq(event.id))\n            .where(\n                eqUserId(condition.userId),\n                openingState(condition.showing),\n                order.orderStatus.notIn(\n                    OrderStatus.FAILED,\n                    OrderStatus.PENDING_PAYMENT,\n                    OrderStatus.READY,\n                    OrderStatus.OUTDATED\n                )\n            )\n            .orderBy(order.id.desc())\n            .offset(pageable.offset)\n            .limit(pageable.pageSize + 1L)\n            .fetch()\n        return SliceUtil.valueOf(orders, pageable)\n    }\n\n    override fun findEventOrders(condition: FindEventOrdersCondition, pageable: Pageable): Page<Order> {\n        val orders = queryFactory\n            .selectFrom(order)\n            .join(user)\n            .on(user.id.eq(order.userId))\n            .where(\n                condition.showDeleteUserExpression(),\n                eqEventId(condition.eventId),\n                condition.getOrderStatusFilter(),\n                condition.getSearchStringFilter()\n            )\n            .orderBy(order.id.desc())\n            .offset(pageable.offset)\n            .limit(pageable.pageSize.toLong())\n            .fetch()\n\n        val countQuery: JPAQuery<Long> = queryFactory\n            .select(order.count())\n            .from(order)\n            .join(user)\n            .on(user.id.eq(order.userId))\n            .where(\n                condition.showDeleteUserExpression(),\n                eqEventId(condition.eventId),\n                condition.getOrderStatusFilter(),\n                condition.getSearchStringFilter()\n            )\n\n        return PageableExecutionUtils.getPage(orders, pageable) { countQuery.fetchOne() ?: 0L }\n    }\n\n    override fun findRecentOrder(userId: Long): Optional<Order> {\n        val findOrder = queryFactory\n            .selectFrom(order)\n            .where(\n                eqUserId(userId),\n                order.orderStatus.`in`(\n                    OrderStatus.PENDING_APPROVE,\n                    OrderStatus.APPROVED,\n                    OrderStatus.CONFIRM\n                )\n            )\n            .orderBy(order.id.desc())\n            .fetchFirst()\n        return Optional.ofNullable(findOrder)\n    }\n\n    private fun eqUserId(userId: Long?): BooleanExpression? =\n        if (userId == null) null else order.userId.eq(userId)\n\n    private fun eqEventId(eventId: Long?): BooleanExpression? =\n        if (eventId == null) null else order.eventId.eq(eventId)\n\n    override fun findRefunds(eventId: Long?, refundStatus: RefundStatus?, keyword: String?, pageable: Pageable): Page<Order> {\n        val orders = queryFactory\n            .selectFrom(order)\n            .where(\n                order.refundStatus.ne(RefundStatus.NONE),\n                eqEventId(eventId),\n                eqRefundStatus(refundStatus),\n                containsOrderNo(keyword),\n            )\n            .orderBy(order.id.desc())\n            .offset(pageable.offset)\n            .limit(pageable.pageSize.toLong())\n            .fetch()\n\n        val countQuery: JPAQuery<Long> = queryFactory\n            .select(order.count())\n            .from(order)\n            .where(\n                order.refundStatus.ne(RefundStatus.NONE),\n                eqEventId(eventId),\n                eqRefundStatus(refundStatus),\n                containsOrderNo(keyword),\n            )\n\n        return PageableExecutionUtils.getPage(orders, pageable) { countQuery.fetchOne() ?: 0L }\n    }\n\n    private fun eqRefundStatus(refundStatus: RefundStatus?): BooleanExpression? =\n        if (refundStatus == null) null else order.refundStatus.eq(refundStatus)\n\n    private fun containsOrderNo(keyword: String?): BooleanExpression? =\n        if (keyword.isNullOrBlank()) null else order.orderNo.containsIgnoreCase(keyword)\n\n    private fun openingState(isShowing: Boolean?): BooleanExpression {\n        val eventEndAtTemplate: DateTemplate<LocalDateTime> = Expressions.dateTemplate(\n            LocalDateTime::class.java,\n            \"TIMESTAMPADD(MINUTE,{0}, {1}) \",\n            event.eventBasic.runTime,\n            event.eventBasic.startAt\n        )\n        val now = LocalDateTime.now()\n        return if (isShowing == true) eventEndAtTemplate.after(now) else eventEndAtTemplate.before(now)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/repository/OrderRepository.kt",
    "content": "package band.gosrock.domain.domains.order.repository\n\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport java.time.LocalDateTime\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.jpa.repository.Query\nimport org.springframework.data.repository.CrudRepository\nimport org.springframework.data.repository.query.Param\n\ninterface OrderRepository : CrudRepository<Order, Long>, OrderCustomRepository {\n    fun countByEventId(eventId: Long): Long\n\n    fun findByEventId(eventId: Long): List<Order>\n    fun findByEventIdAndOrderStatus(eventId: Long, orderStatus: OrderStatus): List<Order>\n    fun findByEventIdAndUserIdAndOrderStatus(eventId: Long, userId: Long, orderStatus: OrderStatus): List<Order>\n    fun findByUuidIn(uuids: List<String>): List<Order>\n\n    /** Admin: 키워드(orderName/유저명/이벤트명) + 상태 + 이벤트 필터로 주문 검색 */\n    @Query(\n        \"\"\"SELECT o FROM tbl_order o WHERE\n            (:keyword IS NULL OR o.orderName LIKE %:keyword%\n                OR EXISTS (SELECT 1 FROM User u WHERE u.id = o.userId AND u.profile.name LIKE %:keyword%)\n                OR EXISTS (SELECT 1 FROM tbl_event e WHERE e.id = o.eventId AND e.eventBasic.name LIKE %:keyword%))\n            AND (:orderStatus IS NULL OR o.orderStatus = :orderStatus)\n            AND (:eventId IS NULL OR o.eventId = :eventId)\"\"\"\n    )\n    fun findAllForAdmin(\n        @Param(\"keyword\") keyword: String?,\n        @Param(\"orderStatus\") orderStatus: OrderStatus?,\n        @Param(\"eventId\") eventId: Long?,\n        pageable: Pageable\n    ): Page<Order>\n\n    /** Admin: 오늘 이후 생성된 주문 수 */\n    fun countByCreatedAtAfter(after: LocalDateTime): Long\n\n    /** Admin: 오늘 이후 생성된 특정 상태 주문 수 */\n    fun countByCreatedAtAfterAndOrderStatus(after: LocalDateTime, orderStatus: OrderStatus): Long\n\n    /** Admin: 오늘 이후 생성된 특정 상태 주문 목록 (매출 계산용) */\n    fun findByCreatedAtAfterAndOrderStatusIn(after: LocalDateTime, orderStatuses: List<OrderStatus>): List<Order>\n\n    /** Admin: 기간 내 주문 수 */\n    fun countByCreatedAtBetween(start: LocalDateTime, end: LocalDateTime): Long\n\n    /** Admin: 기간 내 특정 상태 주문 수 */\n    fun countByCreatedAtBetweenAndOrderStatus(start: LocalDateTime, end: LocalDateTime, orderStatus: OrderStatus): Long\n\n    /** Admin: 기간 내 특정 상태 주문 목록 (매출 계산용) */\n    fun findByCreatedAtBetweenAndOrderStatusIn(start: LocalDateTime, end: LocalDateTime, orderStatuses: List<OrderStatus>): List<Order>\n\n    /** Admin: 최근 주문 N건 */\n    @Query(\"SELECT o FROM tbl_order o ORDER BY o.createdAt DESC\")\n    fun findTopNByOrderByCreatedAtDesc(pageable: Pageable): List<Order>\n\n    /** Admin: 페이지네이션 없이 전체 주문 조회 (엑셀 다운로드용) */\n    @Query(\n        \"\"\"SELECT o FROM tbl_order o WHERE\n            (:keyword IS NULL OR o.orderName LIKE %:keyword%\n                OR EXISTS (SELECT 1 FROM User u WHERE u.id = o.userId AND u.profile.name LIKE %:keyword%)\n                OR EXISTS (SELECT 1 FROM tbl_event e WHERE e.id = o.eventId AND e.eventBasic.name LIKE %:keyword%))\n            AND (:orderStatus IS NULL OR o.orderStatus = :orderStatus)\n            AND (:eventId IS NULL OR o.eventId = :eventId)\"\"\"\n    )\n    fun findAllForAdminNoPage(\n        @Param(\"keyword\") keyword: String?,\n        @Param(\"orderStatus\") orderStatus: OrderStatus?,\n        @Param(\"eventId\") eventId: Long?,\n    ): List<Order>\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/repository/condition/AdminTableOrderFilterType.kt",
    "content": "package band.gosrock.domain.domains.order.repository.condition\n\nimport band.gosrock.domain.domains.order.domain.OrderStatus.APPROVED\nimport band.gosrock.domain.domains.order.domain.OrderStatus.CANCELED\nimport band.gosrock.domain.domains.order.domain.OrderStatus.CONFIRM\nimport band.gosrock.domain.domains.order.domain.OrderStatus.PENDING_APPROVE\nimport band.gosrock.domain.domains.order.domain.OrderStatus.REFUND\nimport band.gosrock.domain.domains.order.domain.QOrder.order\nimport band.gosrock.domain.domains.user.domain.AccountState\nimport band.gosrock.domain.domains.user.domain.QUser.user\nimport com.querydsl.core.types.dsl.BooleanExpression\n\n/** 어드민 테이블의 주문 상태 검색 조건 지정을 위함. */\nenum class AdminTableOrderFilterType(\n    private val expression: BooleanExpression,\n    private val showDeleteUserExpression: BooleanExpression?\n) {\n    APPROVE_WAITING(\n        order.orderStatus.eq(PENDING_APPROVE),\n        user.accountState.ne(AccountState.DELETED)\n    ),\n    // 완료된 주문을 가져오는건 지워진 유저도 불러와야한다.\n    CONFIRMED(\n        order.orderStatus.`in`(CONFIRM, APPROVED, CANCELED, REFUND),\n        null\n    );\n\n    fun getFilter(): BooleanExpression = expression\n\n    fun showDeleteUserExpression(): BooleanExpression? = showDeleteUserExpression\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/repository/condition/AdminTableSearchType.kt",
    "content": "package band.gosrock.domain.domains.order.repository.condition\n\nimport band.gosrock.domain.domains.user.domain.QUser.user\nimport com.querydsl.core.types.dsl.BooleanExpression\n\n/** 어드민 테이블의 검색어 지정 ( 전화번호 이름 검색 ) 을 지원하기 위함. */\nenum class AdminTableSearchType(\n    private val expression: (String) -> BooleanExpression\n) {\n    // 검색 형식은 지원... 저장 형식이 이럼 xxxx-xxxx\n    PHONE({ keyword -> user.profile.phoneNumberVo.phoneNumber.contains(keyword) }),\n    NAME({ keyword -> user.profile.name.contains(keyword) });\n\n    fun getContains(keyWord: String): BooleanExpression = expression(keyWord)\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/repository/condition/FindEventOrdersCondition.kt",
    "content": "package band.gosrock.domain.domains.order.repository.condition\n\nimport com.querydsl.core.types.dsl.BooleanExpression\n\nclass FindEventOrdersCondition @JvmOverloads constructor(\n    val eventId: Long,\n    searchString: String? = null,\n    val searchType: AdminTableSearchType?,\n    val filterType: AdminTableOrderFilterType,\n) {\n    val searchString: String = searchString ?: \"\"\n\n    fun getOrderStatusFilter(): BooleanExpression = filterType.getFilter()\n\n    fun showDeleteUserExpression(): BooleanExpression? = filterType.showDeleteUserExpression()\n\n    fun getSearchStringFilter(): BooleanExpression? {\n        if (searchType == null) return null\n        return searchType.getContains(searchString)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/repository/condition/FindMyPageOrderCondition.kt",
    "content": "package band.gosrock.domain.domains.order.repository.condition\n\ndata class FindMyPageOrderCondition(\n    val userId: Long,\n    val showing: Boolean,\n) {\n    companion object {\n        @JvmStatic\n        fun onShowing(userId: Long): FindMyPageOrderCondition =\n            FindMyPageOrderCondition(userId = userId, showing = true)\n\n        @JvmStatic\n        fun notShowing(userId: Long): FindMyPageOrderCondition =\n            FindMyPageOrderCondition(userId = userId, showing = false)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/service/CreateOrderService.kt",
    "content": "package band.gosrock.domain.domains.order.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.common.aop.redissonLock.RedissonLock\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport org.springframework.transaction.annotation.Transactional\n\n@DomainService\n@Transactional(readOnly = true)\nclass CreateOrderService(\n    private val orderFactory: OrderFactory,\n    private val orderAdaptor: OrderAdaptor,\n) {\n    @RedissonLock(LockName = \"주문생성\", identifier = \"userId\")\n    fun withOutCoupon(cartId: Long, userId: Long): String {\n        val order = orderFactory.createNormalOrder(cartId, userId)\n        return orderAdaptor.save(order).uuid!!\n    }\n\n    @RedissonLock(LockName = \"주문생성\", identifier = \"userId\")\n    fun withCoupon(cartId: Long, userId: Long, couponId: Long): String {\n        val order = orderFactory.createCouponOrder(cartId, userId, couponId)\n        return orderAdaptor.save(order).uuid!!\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/service/FreeOrderService.kt",
    "content": "package band.gosrock.domain.domains.order.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.common.aop.redissonLock.RedissonLock\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.domain.validator.OrderValidator\nimport org.springframework.transaction.annotation.Transactional\n\n@DomainService\n@Transactional(readOnly = true)\nclass FreeOrderService(\n    private val orderAdaptor: OrderAdaptor,\n    private val orderValidator: OrderValidator,\n) {\n    @RedissonLock(LockName = \"주문\", identifier = \"orderUuid\")\n    fun execute(orderUuid: String, currentUserId: Long): String {\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        order.freeConfirm(currentUserId, orderValidator)\n        return orderUuid\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/service/OrderApproveService.kt",
    "content": "package band.gosrock.domain.domains.order.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.common.aop.redissonLock.RedissonLock\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.domain.validator.OrderValidator\nimport org.springframework.transaction.annotation.Transactional\n\n@DomainService\n@Transactional(readOnly = true)\nclass OrderApproveService(\n    private val orderAdaptor: OrderAdaptor,\n    private val orderValidator: OrderValidator,\n) {\n    @RedissonLock(LockName = \"주문\", identifier = \"orderUuid\")\n    fun execute(orderUuid: String): String {\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        order.approve(orderValidator)\n        return orderUuid\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/service/OrderConfirmService.kt",
    "content": "package band.gosrock.domain.domains.order.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.common.aop.redissonLock.RedissonLock\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.domain.PgPaymentInfo\nimport band.gosrock.domain.domains.order.domain.validator.OrderValidator\nimport band.gosrock.infrastructure.outer.api.tossPayments.client.PaymentsConfirmClient\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.request.ConfirmPaymentsRequest\nimport org.springframework.transaction.annotation.Transactional\n\n@DomainService\n@Transactional(readOnly = true)\nclass OrderConfirmService(\n    private val paymentsConfirmClient: PaymentsConfirmClient,\n    private val orderAdaptor: OrderAdaptor,\n    private val orderValidator: OrderValidator,\n) {\n    @RedissonLock(LockName = \"주문\", identifier = \"orderId\", paramClassType = ConfirmPaymentsRequest::class)\n    fun execute(confirmPaymentsRequest: ConfirmPaymentsRequest, currentUserId: Long): String {\n        val order = orderAdaptor.findByOrderUuid(confirmPaymentsRequest.orderId!!)\n        val paymentWons = Money.wons(confirmPaymentsRequest.amount!!)\n        orderValidator.validOwner(order, currentUserId)\n        orderValidator.validMethodIsPaymentOrder(order)\n        orderValidator.validAmountIsSameAsRequest(order, paymentWons)\n        val paymentsResponse = paymentsConfirmClient.execute(confirmPaymentsRequest)\n        order.confirmPayment(\n            paymentsResponse.approvedAt!!.toLocalDateTime(),\n            PgPaymentInfo.from(paymentsResponse),\n            orderValidator,\n        )\n        return order.uuid!!\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/service/OrderFactory.kt",
    "content": "package band.gosrock.domain.domains.order.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.domains.cart.adaptor.CartAdaptor\nimport band.gosrock.domain.domains.coupon.adaptor.IssuedCouponAdaptor\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.validator.OrderValidator\nimport band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor\nimport band.gosrock.domain.domains.ticket_item.domain.TicketPayType\nimport org.springframework.transaction.annotation.Transactional\n\n@DomainService\n@Transactional(readOnly = true)\nclass OrderFactory(\n    private val cartAdaptor: CartAdaptor,\n    private val itemAdaptor: TicketItemAdaptor,\n    private val issuedCouponAdaptor: IssuedCouponAdaptor,\n    private val orderValidator: OrderValidator,\n) {\n    fun createNormalOrder(cartId: Long, userId: Long): Order {\n        val cart = cartAdaptor.queryCart(cartId, userId)\n        val ticketItem = itemAdaptor.queryTicketItem(cart.getItemId())\n        val payType = ticketItem.payType\n        return when {\n            payType == TicketPayType.DUDOONG_TICKET -> Order.createApproveOrder(userId, cart, ticketItem, orderValidator)\n            payType == TicketPayType.FREE_TICKET && ticketItem.isFCFS() -> Order.createPaymentOrder(userId, cart, ticketItem, orderValidator)\n            payType == TicketPayType.FREE_TICKET -> Order.createApproveOrder(userId, cart, ticketItem, orderValidator)\n            else -> Order.createPaymentOrder(userId, cart, ticketItem, orderValidator)\n        }\n    }\n\n    fun createCouponOrder(cartId: Long, userId: Long, couponId: Long): Order {\n        val coupon = issuedCouponAdaptor.query(couponId)\n        val cart = cartAdaptor.queryCart(cartId, userId)\n        val ticketItem = itemAdaptor.queryTicketItem(cart.getItemId())\n        return Order.createPaymentOrderWithCoupon(userId, cart, ticketItem, coupon, orderValidator)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/service/WithdrawOrderService.kt",
    "content": "package band.gosrock.domain.domains.order.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.common.aop.redissonLock.RedissonLock\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.domain.validator.OrderValidator\nimport org.slf4j.LoggerFactory\nimport org.springframework.transaction.annotation.Transactional\n\n@DomainService\n@Transactional(readOnly = true)\nclass WithdrawOrderService(\n    private val orderAdaptor: OrderAdaptor,\n    private val orderValidator: OrderValidator,\n) {\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    @JvmOverloads\n    @RedissonLock(LockName = \"주문\", identifier = \"orderUuid\")\n    fun cancelOrder(orderUuid: String, reason: String? = null): String {\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        order.cancel(orderValidator, reason)\n        return orderUuid\n    }\n\n    @JvmOverloads\n    @RedissonLock(LockName = \"주문\", identifier = \"orderUuid\")\n    fun refundOrder(orderUuid: String, userId: Long, reason: String? = null): String {\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        order.refund(userId, orderValidator, reason)\n        return orderUuid\n    }\n\n    @JvmOverloads\n    @RedissonLock(LockName = \"주문\", identifier = \"orderUuid\")\n    fun refuseOrder(orderUuid: String, reason: String? = null): String {\n        val order = orderAdaptor.findByOrderUuid(orderUuid)\n        order.refuse(orderValidator, reason)\n        return orderUuid\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/service/WithdrawPaymentService.kt",
    "content": "package band.gosrock.domain.domains.order.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.infrastructure.outer.api.tossPayments.client.PaymentsCancelClient\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.request.CancelPaymentsRequest\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.response.PaymentsResponse\nimport org.slf4j.LoggerFactory\n\n@DomainService\nclass WithdrawPaymentService(private val paymentsCancelClient: PaymentsCancelClient) {\n\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    fun execute(orderUuid: String, paymentKey: String, reason: String): PaymentsResponse {\n        log.info(\"취소처리 $orderUuid : $paymentKey$reason\")\n        return paymentsCancelClient.execute(\n            orderUuid,\n            paymentKey,\n            CancelPaymentsRequest(cancelReason = reason),\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/service/handler/ConfirmOrderFailHandler.kt",
    "content": "package band.gosrock.domain.domains.order.service.handler\n\nimport band.gosrock.domain.common.events.order.DoneOrderEvent\nimport band.gosrock.domain.domains.coupon.service.RecoveryCouponService\nimport band.gosrock.domain.domains.issuedTicket.service.IssuedTicketDomainService\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.service.WithdrawPaymentService\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.annotation.Propagation\nimport org.springframework.transaction.annotation.Transactional\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\n@Component\nclass ConfirmOrderFailHandler(\n    private val cancelPaymentService: WithdrawPaymentService,\n    private val issuedTicketDomainService: IssuedTicketDomainService,\n    private val recoveryCouponService: RecoveryCouponService,\n    private val orderAdaptor: OrderAdaptor,\n) {\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    @Async\n    @Transactional(propagation = Propagation.REQUIRES_NEW)\n    @TransactionalEventListener(classes = [DoneOrderEvent::class], phase = TransactionPhase.AFTER_ROLLBACK)\n    fun handleDoneOrderFailEvent(doneOrderEvent: DoneOrderEvent) {\n        log.info(\"${doneOrderEvent.orderUuid} 주문 실패 처리 핸들러\")\n        val order = orderAdaptor.findByOrderUuid(doneOrderEvent.orderUuid)\n        order.fail(\"결제 확인 과정에서 실패\")\n\n        if (order.hasCoupon()) {\n            recoveryCouponService.execute(order.userId!!, order.orderCouponVo.couponId)\n        }\n\n        issuedTicketDomainService.doneOrderEventAfterRollBackWithdrawIssuedTickets(\n            doneOrderEvent.itemId, doneOrderEvent.orderUuid\n        )\n\n        if (order.isNeedPaid()) {\n            log.info(\"${doneOrderEvent.orderUuid}:${doneOrderEvent.paymentKey} 주문 실패 시 결제 취소\")\n            cancelPaymentService.execute(order.uuid!!, doneOrderEvent.paymentKey!!, \"서버 오류로 인한 환불\")\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/order/service/handler/WithDrawOrderHandler.kt",
    "content": "package band.gosrock.domain.domains.order.service.handler\n\nimport band.gosrock.domain.common.events.order.WithDrawOrderEvent\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport band.gosrock.domain.domains.order.service.WithdrawPaymentService\nimport org.slf4j.LoggerFactory\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\nimport org.springframework.transaction.event.TransactionPhase\nimport org.springframework.transaction.event.TransactionalEventListener\n\n@Component\nclass WithDrawOrderHandler(\n    private val withdrawPaymentService: WithdrawPaymentService,\n    private val orderAdaptor: OrderAdaptor,\n) {\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    @Async\n    @TransactionalEventListener(classes = [WithDrawOrderEvent::class], phase = TransactionPhase.AFTER_COMMIT)\n    fun handleWithDrawOrderEvent(withDrawOrderEvent: WithDrawOrderEvent) {\n        log.info(\"${withDrawOrderEvent.orderUuid} 주문 철회 핸들러\")\n        val orderStatus = withDrawOrderEvent.orderStatus\n        val order = orderAdaptor.findByOrderUuid(withDrawOrderEvent.orderUuid)\n        if (!order.isPaid()) return\n\n        val reason = when (orderStatus) {\n            OrderStatus.CANCELED -> \"이벤트 관리자에 의한 취소\"\n            OrderStatus.REFUND -> \"구매자에의한 환불 요청\"\n            else -> \"결제 취소\"\n        }\n\n        log.info(\"${withDrawOrderEvent.orderUuid} 주문 철회 ${order.paymentKey}$reason\")\n        withdrawPaymentService.execute(withDrawOrderEvent.orderUuid, order.paymentKey, reason)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/settlement/adaptor/EventSettlementAdaptor.kt",
    "content": "package band.gosrock.domain.domains.settlement.adaptor\n\nimport band.gosrock.common.annotation.Adaptor\nimport band.gosrock.domain.domains.settlement.domain.EventSettlement\nimport band.gosrock.domain.domains.settlement.repository.EventSettlementRepository\n\n@Adaptor\nclass EventSettlementAdaptor(\n    private val eventSettlementRepository: EventSettlementRepository,\n) {\n    fun save(eventSettlement: EventSettlement): EventSettlement =\n        eventSettlementRepository.save(eventSettlement)\n\n    fun findByEventId(eventId: Long): EventSettlement =\n        eventSettlementRepository.findByEventId(eventId).orElseThrow()\n\n    fun upsertByEventId(eventId: Long): EventSettlement =\n        eventSettlementRepository.findByEventId(eventId)\n            .orElseGet { eventSettlementRepository.save(EventSettlement.createWithEventId(eventId)) }\n\n    fun deleteByEventId(eventId: Long) =\n        eventSettlementRepository.deleteByEventId(eventId)\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/settlement/adaptor/TransactionSettlementAdaptor.kt",
    "content": "package band.gosrock.domain.domains.settlement.adaptor\n\nimport band.gosrock.common.annotation.Adaptor\nimport band.gosrock.domain.domains.settlement.domain.TransactionSettlement\nimport band.gosrock.domain.domains.settlement.repository.TransactionSettlementRepository\n\n@Adaptor\nclass TransactionSettlementAdaptor(\n    private val transactionSettlementRepository: TransactionSettlementRepository,\n) {\n    fun saveAll(transactionSettlements: List<TransactionSettlement>) {\n        transactionSettlementRepository.saveAll(transactionSettlements)\n    }\n\n    fun findByEventId(eventId: Long): List<TransactionSettlement> =\n        transactionSettlementRepository.findByEventId(eventId)\n\n    fun deleteByEventId(eventId: Long) =\n        transactionSettlementRepository.deleteByEventId(eventId)\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/settlement/domain/EventSettlement.kt",
    "content": "package band.gosrock.domain.domains.settlement.domain\n\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport band.gosrock.domain.common.vo.Money\nimport jakarta.persistence.AttributeOverride\nimport jakarta.persistence.Column\nimport jakarta.persistence.Embedded\nimport jakarta.persistence.Entity\nimport jakarta.persistence.EnumType\nimport jakarta.persistence.Enumerated\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\n\n/** 이벤트 별 정산용 ( 클라이언트 용 ) */\n@Entity(name = \"tbl_event_settlement\")\nopen class EventSettlement(\n    var eventId: Long? = null,\n\n    // 총 매출 금액\n    @AttributeOverride(name = \"amount\", column = Column(name = \"total_sales_amount\"))\n    @Embedded\n    var totalSalesAmount: Money? = null,\n\n    // 두둥티켓 송금 관련\n    @AttributeOverride(name = \"amount\", column = Column(name = \"dudoong_amount\"))\n    @Embedded\n    var dudoongAmount: Money? = null,\n\n    // 카드 결제 금액\n    @AttributeOverride(name = \"amount\", column = Column(name = \"payment_amount\"))\n    @Embedded\n    var paymentAmount: Money? = null,\n\n    // 쿠폰 금액\n    @AttributeOverride(name = \"amount\", column = Column(name = \"coupon_amount\"))\n    @Embedded\n    var couponAmount: Money? = null,\n\n    // 중개 수수료 ( 카드 결제 금액의 % )\n    @AttributeOverride(name = \"amount\", column = Column(name = \"dudoong_fee\"))\n    @Embedded\n    var dudoongFee: Money? = null,\n\n    // 결제 대행 수수료\n    @AttributeOverride(name = \"amount\", column = Column(name = \"pg_fee\"))\n    @Embedded\n    var pgFee: Money? = null,\n\n    // 결제 대행 수수료 vat\n    @AttributeOverride(name = \"amount\", column = Column(name = \"pg_fee_vat\"))\n    @Embedded\n    var pgFeeVat: Money? = null,\n\n    // 최종 정산 금액\n    @AttributeOverride(name = \"amount\", column = Column(name = \"total_amount\"))\n    @Embedded\n    var totalAmount: Money? = null,\n\n    // S3 업로드된 키.\n    var eventOrderExcelKey: String? = null,\n\n    // 정산 진행 과정\n    @Enumerated(EnumType.STRING)\n    var eventSettlementStatus: EventSettlementStatus = EventSettlementStatus.READY,\n) : BaseTimeEntity() {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"event_settlement_id\")\n    var id: Long? = null\n        protected set\n\n    fun updateEventOrderListExcelKey(key: String) {\n        this.eventOrderExcelKey = key\n    }\n\n    companion object {\n        @JvmStatic\n        fun createWithEventId(eventId: Long): EventSettlement =\n            EventSettlement(eventId = eventId)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/settlement/domain/EventSettlementStatus.kt",
    "content": "package band.gosrock.domain.domains.settlement.domain\n\nenum class EventSettlementStatus {\n    READY,\n    CALCULATED,\n    ADMIN_CONFIRM,\n    CLIENT_CONFIRM\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/settlement/domain/SettlementFeeVo.kt",
    "content": "package band.gosrock.domain.domains.settlement.domain\n\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.response.FeeCode\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.response.SettlementFeeDto\nimport jakarta.persistence.Column\nimport jakarta.persistence.Embeddable\nimport jakarta.persistence.EnumType\nimport jakarta.persistence.Enumerated\n\n@Embeddable\nclass SettlementFeeVo(\n    @Enumerated(EnumType.STRING)\n    var type: FeeCode? = null,\n\n    @Column(name = \"fee\")\n    var fee: Long? = null,\n) {\n    companion object {\n        @JvmStatic\n        fun from(settlementFeeDto: SettlementFeeDto): SettlementFeeVo =\n            SettlementFeeVo(\n                type = settlementFeeDto.type!!,\n                fee = settlementFeeDto.fee!!,\n            )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/settlement/domain/TransactionSettlement.kt",
    "content": "package band.gosrock.domain.domains.settlement.domain\n\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.response.SettlementResponse\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.response.TossPaymentMethod\nimport java.time.LocalDate\nimport java.time.LocalDateTime\nimport jakarta.persistence.AttributeOverride\nimport jakarta.persistence.CollectionTable\nimport jakarta.persistence.Column\nimport jakarta.persistence.ElementCollection\nimport jakarta.persistence.Embedded\nimport jakarta.persistence.Entity\nimport jakarta.persistence.EnumType\nimport jakarta.persistence.Enumerated\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\n\n/** 거래 건 별 정산용 ( 로그 성 ) */\n@Entity(name = \"tbl_transaction_settlement\")\nopen class TransactionSettlement protected constructor() : BaseTimeEntity() {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"transaction_settlement_id\")\n    var id: Long? = null\n        protected set\n\n    var eventId: Long? = null\n        protected set\n\n    // 주문 아이디\n    var orderUuid: String? = null\n        protected set\n\n    // 결제 키값 ( 주문 요청시 토스페이먼츠 자체 키값 )\n    var paymentKey: String? = null\n        protected set\n\n    // 거래 키값 ( 트랜잭션 키 )\n    var transactionKey: String? = null\n        protected set\n\n    // 결제 방식\n    @Enumerated(EnumType.STRING)\n    var paymentMethod: TossPaymentMethod? = null\n        protected set\n\n    // 결제한 금액\n    @AttributeOverride(name = \"amount\", column = Column(name = \"payment_amount\"))\n    @Embedded\n    var paymentAmount: Money? = null\n        protected set\n\n    // 수수료 상세정보\n    @ElementCollection\n    @CollectionTable(name = \"tbl_transaction_settlement_fee_detail\")\n    var fees: MutableList<SettlementFeeVo> = mutableListOf()\n        protected set\n\n    // 수수료 공급가액 ( 수수료 총액 )\n    @AttributeOverride(name = \"amount\", column = Column(name = \"fee_supply_amount\"))\n    @Embedded\n    var feeSupplyAmount: Money? = null\n        protected set\n\n    // 수수료 부가세\n    @AttributeOverride(name = \"amount\", column = Column(name = \"fee_vat\"))\n    @Embedded\n    var feeVat: Money? = null\n        protected set\n\n    // 할부 수수료 금액\n    @AttributeOverride(name = \"amount\", column = Column(name = \"interest_fee\"))\n    @Embedded\n    var interestFee: Money? = null\n        protected set\n\n    // 지급 금액 ( 정산 받는 금액 )\n    @AttributeOverride(name = \"amount\", column = Column(name = \"settlement_amount\"))\n    @Embedded\n    var settlementAmount: Money? = null\n        protected set\n\n    // 정산 지급일 ( 예정일도 가능 )\n    var soldDate: LocalDate? = null\n        protected set\n\n    // 정산 매출일\n    var paidOutDate: LocalDate? = null\n        protected set\n\n    // 거래 승인 시점\n    var approvedAt: LocalDateTime? = null\n        protected set\n\n    constructor(\n        eventId: Long?,\n        orderUuid: String?,\n        paymentKey: String?,\n        transactionKey: String?,\n        paymentMethod: TossPaymentMethod?,\n        paymentAmount: Money?,\n        fees: List<SettlementFeeVo>,\n        feeSupplyAmount: Money?,\n        feeVat: Money?,\n        interestFee: Money?,\n        settlementAmount: Money?,\n        soldDate: LocalDate?,\n        paidOutDate: LocalDate?,\n        approvedAt: LocalDateTime?,\n    ) : this() {\n        this.eventId = eventId\n        this.orderUuid = orderUuid\n        this.paymentKey = paymentKey\n        this.transactionKey = transactionKey\n        this.paymentMethod = paymentMethod\n        this.paymentAmount = paymentAmount\n        this.fees = fees.toMutableList()\n        this.feeSupplyAmount = feeSupplyAmount\n        this.feeVat = feeVat\n        this.interestFee = interestFee\n        this.settlementAmount = settlementAmount\n        this.soldDate = soldDate\n        this.paidOutDate = paidOutDate\n        this.approvedAt = approvedAt\n    }\n\n    companion object {\n        @JvmStatic\n        fun of(eventId: Long, settlementResponse: SettlementResponse): TransactionSettlement {\n            val settlementFeeVos = (settlementResponse.fees\n                ?: throw IllegalArgumentException(\"Missing fees in settlement response\"))\n                .map { SettlementFeeVo.from(it) }\n            return TransactionSettlement(\n                eventId = eventId,\n                orderUuid = settlementResponse.orderId,\n                paymentKey = settlementResponse.paymentKey,\n                transactionKey = settlementResponse.transactionKey,\n                paymentMethod = settlementResponse.method,\n                paymentAmount = Money.wons(settlementResponse.amount\n                    ?: throw IllegalArgumentException(\"Missing amount in settlement response\")),\n                fees = settlementFeeVos,\n                feeSupplyAmount = Money.wons(settlementResponse.supplyAmount\n                    ?: throw IllegalArgumentException(\"Missing supplyAmount in settlement response\")),\n                feeVat = Money.wons(settlementResponse.vat\n                    ?: throw IllegalArgumentException(\"Missing vat in settlement response\")),\n                interestFee = Money.wons(settlementResponse.interestFee\n                    ?: throw IllegalArgumentException(\"Missing interestFee in settlement response\")),\n                settlementAmount = Money.wons(settlementResponse.payOutAmount\n                    ?: throw IllegalArgumentException(\"Missing payOutAmount in settlement response\")),\n                soldDate = settlementResponse.soldDate,\n                paidOutDate = settlementResponse.paidOutDate,\n                approvedAt = (settlementResponse.approvedAt\n                    ?: throw IllegalArgumentException(\"Missing approvedAt in settlement response\"))\n                    .toLocalDateTime(),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/settlement/repository/EventSettlementRepository.kt",
    "content": "package band.gosrock.domain.domains.settlement.repository\n\nimport band.gosrock.domain.domains.settlement.domain.EventSettlement\nimport org.springframework.data.repository.CrudRepository\nimport java.util.Optional\n\ninterface EventSettlementRepository : CrudRepository<EventSettlement, Long> {\n    fun findByEventId(eventId: Long): Optional<EventSettlement>\n    fun deleteByEventId(eventId: Long)\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/settlement/repository/TransactionSettlementRepository.kt",
    "content": "package band.gosrock.domain.domains.settlement.repository\n\nimport band.gosrock.domain.domains.settlement.domain.TransactionSettlement\nimport org.springframework.data.repository.CrudRepository\n\ninterface TransactionSettlementRepository : CrudRepository<TransactionSettlement, Long> {\n    fun findByEventId(eventId: Long): List<TransactionSettlement>\n    fun deleteByEventId(eventId: Long)\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/settlement/service/EventSettlementDomainService.kt",
    "content": "package band.gosrock.domain.domains.settlement.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport band.gosrock.domain.domains.settlement.adaptor.EventSettlementAdaptor\nimport band.gosrock.domain.domains.settlement.adaptor.TransactionSettlementAdaptor\nimport band.gosrock.domain.domains.settlement.domain.EventSettlement\nimport band.gosrock.domain.domains.settlement.domain.EventSettlementStatus\nimport band.gosrock.domain.domains.settlement.domain.TransactionSettlement\nimport org.springframework.transaction.annotation.Transactional\n\n@DomainService\nclass EventSettlementDomainService(\n    private val eventSettlementAdaptor: EventSettlementAdaptor,\n    private val transactionSettlementAdaptor: TransactionSettlementAdaptor,\n) {\n    @Transactional\n    fun generateEventSettlement(eventId: Long, orders: List<Order>) {\n        val transactionSettlements = transactionSettlementAdaptor.findByEventId(eventId)\n        val totalPaymentAmount = getTotalPaymentAmount(transactionSettlements)\n        val pgFee = getPgFee(transactionSettlements, totalPaymentAmount)\n        val pgFeeVat = getPgFeeVat(pgFee)\n\n        val eventSettlement = EventSettlement(\n            eventId = eventId,\n            totalSalesAmount = getTotalSalesAmount(orders),\n            dudoongAmount = getDudoongTicketSalesAmount(orders),\n            paymentAmount = totalPaymentAmount,\n            couponAmount = getPaymentOrderDiscountAmount(orders),\n            dudoongFee = Money.ZERO,\n            pgFee = pgFee,\n            pgFeeVat = pgFeeVat,\n            totalAmount = getTotalSettlementAmount(totalPaymentAmount, pgFee, pgFeeVat),\n            eventSettlementStatus = EventSettlementStatus.CALCULATED,\n        )\n\n        // 최종 정산 금액 계산.\n        eventSettlementAdaptor.save(eventSettlement)\n    }\n\n    /** 토스페이먼츠 수수료 */\n    private fun getPgFee(\n        transactionSettlements: List<TransactionSettlement>,\n        paymentAmount: Money,\n    ): Money = paymentAmount.minus(getSettlementAmount(transactionSettlements))\n\n    /** 최종 정산금액 ( 판매대금 - 토스페이먼츠 수수료 - 토스페이먼츠 수수료 vat ) */\n    private fun getTotalSettlementAmount(\n        paymentAmount: Money,\n        pgFee: Money,\n        pgFeeVat: Money,\n    ): Money = paymentAmount.minus(pgFee).minus(pgFeeVat)\n\n    /** 토스페이먼츠 수수료 vat */\n    private fun getPgFeeVat(pgFee: Money): Money =\n        Money.wons(pgFee.times(0.1).longValue())\n\n    private fun getPaymentOrderDiscountAmount(orders: List<Order>): Money =\n        orders\n            .filter { it.orderStatus == OrderStatus.CONFIRM }\n            .map { it.getTotalDiscountPrice() }\n            .fold(Money.ZERO, Money::plus)\n\n    private fun getPaymentOrderTotalSales(orders: List<Order>): Money =\n        orders\n            .filter { it.orderStatus == OrderStatus.CONFIRM }\n            .map { it.getTotalPaymentPrice() }\n            .fold(Money.ZERO, Money::plus)\n\n    /** 주문목록 두둥티켓 판매 대금 */\n    private fun getDudoongTicketSalesAmount(orders: List<Order>): Money =\n        orders\n            .filter { it.orderStatus == OrderStatus.APPROVED }\n            .map { it.getTotalPaymentPrice() }\n            .fold(Money.ZERO, Money::plus)\n\n    /** 주문 액 기준 총 판매 대금 ( 두둥티켓 , 결제티켓 ) */\n    private fun getTotalSalesAmount(orders: List<Order>): Money =\n        orders\n            .filter { it.orderStatus.isCanWithDraw() }\n            .map { it.getTotalPaymentPrice() }\n            .fold(Money.ZERO, Money::plus)\n\n    /** 토스페이먼츠에서 정산해주는 금액 */\n    private fun getSettlementAmount(transactionSettlements: List<TransactionSettlement>): Money =\n        transactionSettlements\n            .mapNotNull { it.settlementAmount }\n            .fold(Money.ZERO, Money::plus)\n\n    /** 토스페이먼츠의 총 매출액 */\n    private fun getTotalPaymentAmount(transactionSettlements: List<TransactionSettlement>): Money =\n        transactionSettlements\n            .mapNotNull { it.paymentAmount }\n            .fold(Money.ZERO, Money::plus)\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/adaptor/OptionAdaptor.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.adaptor\n\nimport band.gosrock.common.annotation.Adaptor\nimport band.gosrock.domain.domains.ticket_item.domain.Option\nimport band.gosrock.domain.domains.ticket_item.exception.OptionNotFoundException\nimport band.gosrock.domain.domains.ticket_item.repository.OptionRepository\n\n@Adaptor\nclass OptionAdaptor(\n    private val optionRepository: OptionRepository\n) {\n\n    fun queryOption(optionId: Long): Option {\n        return optionRepository\n            .findById(optionId)\n            .orElseThrow { OptionNotFoundException.EXCEPTION }\n    }\n\n    fun findAllByIds(ids: List<Long>): List<Option> {\n        return optionRepository.findAllById(ids)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/adaptor/OptionGroupAdaptor.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.adaptor\n\nimport band.gosrock.common.annotation.Adaptor\nimport band.gosrock.domain.domains.ticket_item.domain.OptionGroup\nimport band.gosrock.domain.domains.ticket_item.domain.OptionGroupStatus\nimport band.gosrock.domain.domains.ticket_item.exception.OptionGroupNotFoundException\nimport band.gosrock.domain.domains.ticket_item.repository.OptionGroupRepository\n\n@Adaptor\nclass OptionGroupAdaptor(\n    private val optionGroupRepository: OptionGroupRepository\n) {\n\n    fun queryOptionGroup(optionGroupId: Long): OptionGroup {\n        return optionGroupRepository\n            .findByIdAndOptionGroupStatus(optionGroupId, OptionGroupStatus.VALID)\n            .orElseThrow { OptionGroupNotFoundException.EXCEPTION }\n    }\n\n    fun findAllByEventId(eventId: Long): List<OptionGroup> {\n        return optionGroupRepository.findAllByEventIdAndOptionGroupStatus(\n            eventId, OptionGroupStatus.VALID\n        )\n    }\n\n    fun save(optionGroup: OptionGroup): OptionGroup {\n        return optionGroupRepository.save(optionGroup)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/adaptor/TicketItemAdaptor.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.adaptor\n\nimport band.gosrock.common.annotation.Adaptor\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItemStatus\nimport band.gosrock.domain.domains.ticket_item.exception.TicketItemNotFoundException\nimport band.gosrock.domain.domains.ticket_item.repository.TicketItemRepository\n\n@Adaptor\nclass TicketItemAdaptor(\n    private val ticketItemRepository: TicketItemRepository\n) {\n\n    fun queryTicketItem(ticketItemId: Long?): TicketItem {\n        return ticketItemRepository\n            .findByIdAndTicketItemStatus(ticketItemId ?: throw TicketItemNotFoundException.EXCEPTION, TicketItemStatus.VALID)\n            .orElseThrow { TicketItemNotFoundException.EXCEPTION }\n    }\n\n    fun findAllByEventId(eventId: Long?): List<TicketItem> {\n        return ticketItemRepository.findAllByEventIdAndTicketItemStatus(\n            eventId ?: return emptyList(), TicketItemStatus.VALID\n        )\n    }\n\n    fun existsByEventId(eventId: Long): Boolean {\n        return ticketItemRepository.existsByEventId(eventId)\n    }\n\n    fun save(ticketItem: TicketItem): TicketItem {\n        return ticketItemRepository.save(ticketItem)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/domain/ItemOptionGroup.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.domain\n\nimport jakarta.persistence.Column\nimport jakarta.persistence.Entity\nimport jakarta.persistence.FetchType\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\nimport jakarta.persistence.JoinColumn\nimport jakarta.persistence.ManyToOne\nimport jakarta.persistence.Table\nimport jakarta.persistence.UniqueConstraint\n\n@Table(uniqueConstraints = [UniqueConstraint(columnNames = [\"item_id\", \"option_group_id\"])])\n@Entity(name = \"tbl_item_option_group\")\nclass ItemOptionGroup(\n    item: TicketItem? = null,\n    optionGroup: OptionGroup? = null,\n) {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"item_option_group_id\")\n    var id: Long? = null\n        protected set\n\n    @ManyToOne(fetch = FetchType.LAZY)\n    @JoinColumn(name = \"item_id\", updatable = false)\n    var item: TicketItem? = item\n        protected set\n\n    @ManyToOne(fetch = FetchType.LAZY)\n    @JoinColumn(name = \"option_group_id\", updatable = false)\n    var optionGroup: OptionGroup? = optionGroup\n        protected set\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/domain/Option.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.domain\n\nimport band.gosrock.common.consts.DuDoongStatic.KR_NO\nimport band.gosrock.common.consts.DuDoongStatic.KR_YES\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.ticket_item.exception.NotCorrectOptionAnswerException\nimport jakarta.persistence.Column\nimport jakarta.persistence.Entity\nimport jakarta.persistence.FetchType\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\nimport jakarta.persistence.JoinColumn\nimport jakarta.persistence.ManyToOne\n\n@Entity(name = \"tbl_option\")\nclass Option(\n    var answer: String? = null,\n    var additionalPrice: Money? = null,\n) {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"option_id\")\n    var id: Long? = null\n        protected set\n\n    @ManyToOne(fetch = FetchType.LAZY)\n    @JoinColumn(name = \"option_group_id\", updatable = false)\n    var optionGroup: OptionGroup? = null\n        protected set\n\n    constructor(answer: String?, additionalPrice: Money?, optionGroup: OptionGroup?) : this(answer = answer, additionalPrice = additionalPrice) {\n        this.optionGroup = optionGroup\n    }\n\n    fun updateOptionGroup(optionGroup: OptionGroup?) {\n        this.optionGroup = optionGroup\n    }\n\n    fun getOptionGroupId(): Long? = this.optionGroup?.id\n\n    fun getQuestionDescription(): String? = this.optionGroup?.description\n\n    fun getQuestionName(): String? = this.optionGroup?.name\n\n    fun getQuestionType(): OptionGroupType? = this.optionGroup?.type\n\n    fun validCorrectAnswer(answer: String) {\n        val type = optionGroup?.type\n        if (type == OptionGroupType.TRUE_FALSE) {\n            if (!isAnswerTrueFalse(answer)) {\n                throw NotCorrectOptionAnswerException.EXCEPTION\n            }\n        }\n    }\n\n    private fun isAnswerTrueFalse(answer: String): Boolean =\n        answer == KR_YES || answer == KR_NO\n\n    companion object {\n        @JvmStatic\n        fun create(answer: String?, additionalPrice: Money?, optionGroup: OptionGroup?): Option =\n            Option(answer, additionalPrice, optionGroup)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/domain/OptionGroup.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.domain\n\nimport band.gosrock.common.consts.DuDoongStatic.KR_NO\nimport band.gosrock.common.consts.DuDoongStatic.KR_YES\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.ticket_item.domain.OptionGroupType.SUBJECTIVE\nimport band.gosrock.domain.domains.ticket_item.domain.OptionGroupType.TRUE_FALSE\nimport band.gosrock.domain.domains.ticket_item.exception.ForbiddenOptionGroupDeleteException\nimport band.gosrock.domain.domains.ticket_item.exception.InvalidOptionGroupException\nimport band.gosrock.domain.domains.ticket_item.exception.InvalidOptionPriceException\nimport jakarta.persistence.CascadeType\nimport jakarta.persistence.Column\nimport jakarta.persistence.Entity\nimport jakarta.persistence.EnumType\nimport jakarta.persistence.Enumerated\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\nimport jakarta.persistence.OneToMany\nimport org.hibernate.annotations.ColumnDefault\n\n@Entity(name = \"tbl_option_group\")\nclass OptionGroup(\n    var eventId: Long? = null,\n    // 옵션 그룹 응답 형식\n    @Enumerated(EnumType.STRING)\n    var type: OptionGroupType? = null,\n    // 옵션 그룹 이름\n    var name: String? = null,\n    // 옵션 그룹 설명\n    var description: String? = null,\n    // 필수 응답 여부\n    var isEssential: Boolean? = null,\n    initialOptions: List<Option> = emptyList(),\n) {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"option_group_id\")\n    var id: Long? = null\n        protected set\n\n    // 상태\n    @Enumerated(EnumType.STRING)\n    @ColumnDefault(value = \"'VALID'\")\n    var optionGroupStatus: OptionGroupStatus = OptionGroupStatus.VALID\n        protected set\n\n    @OneToMany(cascade = [CascadeType.ALL], mappedBy = \"optionGroup\")\n    val options: MutableList<Option> = mutableListOf()\n\n    init {\n        if (initialOptions.isNotEmpty()) {\n            this.options.addAll(initialOptions)\n            initialOptions.forEach { it.updateOptionGroup(this) }\n        }\n    }\n\n    fun validateEventId(eventId: Long) {\n        if (this.eventId != eventId) throw InvalidOptionGroupException.EXCEPTION\n    }\n\n    fun hasApplication(ticketItems: List<TicketItem>): Boolean =\n        ticketItems.any { it.hasItemOptionGroup(this.id!!) }\n\n    fun createTicketOption(additionalPrice: Money): OptionGroup {\n        when (type) {\n            TRUE_FALSE -> {\n                if (additionalPrice.isLessThan(Money.ZERO)) throw InvalidOptionPriceException.EXCEPTION\n                this.options.add(Option.create(KR_YES, additionalPrice, this))\n                this.options.add(Option.create(KR_NO, Money.ZERO, this))\n            }\n            SUBJECTIVE -> {\n                this.options.add(Option.create(\"\", Money.ZERO, this))\n            }\n            else -> {}\n        }\n        return this\n    }\n\n    fun softDeleteOptionGroup(ticketItems: List<TicketItem>) {\n        // 적용된 옵션은 삭제 불가\n        if (this.hasApplication(ticketItems)) throw ForbiddenOptionGroupDeleteException.EXCEPTION\n        this.optionGroupStatus = OptionGroupStatus.DELETED\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/domain/OptionGroupStatus.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.domain\n\nimport com.fasterxml.jackson.annotation.JsonValue\n\nenum class OptionGroupStatus(\n    val value: String,\n    @JsonValue val kr: String,\n) {\n    // 유효\n    VALID(\"VALID\", \"유효\"),\n    // 삭제됨\n    DELETED(\"DELETED\", \"삭제됨\"),\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/domain/OptionGroupType.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.domain\n\nimport com.fasterxml.jackson.annotation.JsonValue\n\nenum class OptionGroupType(\n    val value: String,\n    @JsonValue val displayName: String,\n) {\n    // T/F\n    TRUE_FALSE(\"TRUE_FALSE\", \"Y/N\"),\n    //\n    MULTIPLE_CHOICE(\"MULTIPLE_CHOICE\", \"객관식\"),\n    //\n    SUBJECTIVE(\"SUBJECTIVE\", \"주관식\"),\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/domain/TicketItem.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.domain\n\nimport band.gosrock.common.consts.DuDoongStatic.MINIMUM_PAYMENT_WON\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport band.gosrock.domain.common.vo.AccountInfoVo\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.ticket_item.exception.DuplicatedItemOptionGroupException\nimport band.gosrock.domain.domains.ticket_item.exception.EmptyAccountInfoException\nimport band.gosrock.domain.domains.ticket_item.exception.ForbiddenOptionChangeException\nimport band.gosrock.domain.domains.ticket_item.exception.ForbiddenOptionPriceException\nimport band.gosrock.domain.domains.ticket_item.exception.ForbiddenTicketItemDeleteException\nimport band.gosrock.domain.domains.ticket_item.exception.InvalidPartnerException\nimport band.gosrock.domain.domains.ticket_item.exception.InvalidTicketItemException\nimport band.gosrock.domain.domains.ticket_item.exception.InvalidTicketPriceException\nimport band.gosrock.domain.domains.ticket_item.exception.InvalidTicketTypeException\nimport band.gosrock.domain.domains.ticket_item.exception.NotAppliedItemOptionGroupException\nimport band.gosrock.domain.domains.ticket_item.exception.TicketItemQuantityException\nimport band.gosrock.domain.domains.ticket_item.exception.TicketItemQuantityLackException\nimport band.gosrock.domain.domains.ticket_item.exception.TicketItemQuantityLargeException\nimport band.gosrock.domain.domains.ticket_item.exception.TicketPurchaseLimitException\nimport java.time.LocalDateTime\nimport jakarta.persistence.CascadeType\nimport jakarta.persistence.Column\nimport jakarta.persistence.Embedded\nimport jakarta.persistence.Entity\nimport jakarta.persistence.EnumType\nimport jakarta.persistence.Enumerated\nimport jakarta.persistence.FetchType\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\nimport jakarta.persistence.OneToMany\nimport org.hibernate.annotations.ColumnDefault\nimport org.springframework.util.StringUtils\n\n@Entity(name = \"tbl_ticket_item\")\nclass TicketItem(\n    // 티켓 지불 타입\n    @Enumerated(EnumType.STRING)\n    var payType: TicketPayType? = null,\n    // 티켓 이름\n    var name: String? = null,\n    // 티켓 설명\n    var description: String? = null,\n    // 티켓 가격\n    var price: Money? = null,\n    // 티켓 재고\n    var quantity: Long? = null,\n    // 티켓 공급량\n    var supplyCount: Long? = null,\n    // 1인당 구매 매수 제한\n    var purchaseLimit: Long? = null,\n    // 티켓 승인 타입\n    @Enumerated(EnumType.STRING)\n    var type: TicketType? = null,\n    bankName: String? = null,\n    accountNumber: String? = null,\n    accountHolder: String? = null,\n    // 재고 공개 여부\n    var isQuantityPublic: Boolean? = null,\n    // 판매 가능 여부\n    var isSellable: Boolean? = null,\n    // 판매 시작 시간\n    var saleStartAt: LocalDateTime? = null,\n    // 판매 종료 시간\n    var saleEndAt: LocalDateTime? = null,\n    var eventId: Long? = null,\n) : BaseTimeEntity() {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"ticket_item_id\")\n    var id: Long? = null\n        protected set\n\n    @Embedded\n    var accountInfo: AccountInfoVo? = if (payType == TicketPayType.DUDOONG_TICKET)\n        AccountInfoVo.valueOf(bankName, accountNumber, accountHolder)\n    else null\n        protected set\n\n    // 상태\n    @Enumerated(EnumType.STRING)\n    @ColumnDefault(value = \"'VALID'\")\n    var ticketItemStatus: TicketItemStatus = TicketItemStatus.VALID\n        protected set\n\n    @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)\n    val itemOptionGroups: MutableList<ItemOptionGroup> = mutableListOf()\n\n    fun addItemOptionGroup(optionGroup: OptionGroup) {\n        // 재고 감소된 티켓상품은 옵션적용 변경 불가\n        if (isQuantityReduced()) throw ForbiddenOptionChangeException.EXCEPTION\n\n        // 무료티켓에 유료 옵션 적용 불가\n        if (payType == TicketPayType.FREE_TICKET &&\n            optionGroup.options.any { it.additionalPrice?.isGreaterThan(Money.ZERO) == true }\n        ) {\n            throw ForbiddenOptionPriceException.EXCEPTION\n        }\n\n        // 중복 체크\n        if (hasItemOptionGroup(optionGroup.id!!)) throw DuplicatedItemOptionGroupException.EXCEPTION\n\n        val itemOptionGroup = ItemOptionGroup(item = this, optionGroup = optionGroup)\n        this.itemOptionGroups.add(itemOptionGroup)\n    }\n\n    fun removeItemOptionGroup(optionGroup: OptionGroup) {\n        // 재고 감소된 티켓상품은 옵션적용 변경 불가\n        if (isQuantityReduced()) throw ForbiddenOptionChangeException.EXCEPTION\n        val itemOptionGroup = findItemOptionGroup(optionGroup)\n        this.itemOptionGroups.remove(itemOptionGroup)\n    }\n\n    fun findItemOptionGroup(optionGroup: OptionGroup): ItemOptionGroup =\n        this.itemOptionGroups.firstOrNull { it.optionGroup == optionGroup }\n            ?: throw NotAppliedItemOptionGroupException.EXCEPTION\n\n    fun hasItemOptionGroup(optionGroupId: Long): Boolean =\n        this.itemOptionGroups.any { it.optionGroup?.id == optionGroupId }\n\n    fun softDeleteTicketItem() {\n        // 재고 감소된 티켓상품은 삭제 불가\n        if (isQuantityReduced()) throw ForbiddenTicketItemDeleteException.EXCEPTION\n        this.ticketItemStatus = TicketItemStatus.DELETED\n    }\n\n    fun validateEventId(eventId: Long) {\n        if (this.eventId != eventId) throw InvalidTicketItemException.EXCEPTION\n    }\n\n    fun validateTicketPayType(isPartner: Boolean) {\n        // 두둥티켓은 무조건 승인 + 계좌정보 필요\n        if (payType == TicketPayType.DUDOONG_TICKET) {\n            if (type != TicketType.APPROVAL) throw InvalidTicketTypeException.EXCEPTION\n            // 두둥 티켓은 유로 티켓 이어야함.\n            if (price?.isLessThanOrEqual(Money.ZERO) == true) throw InvalidTicketPriceException.EXCEPTION\n            if (!StringUtils.hasText(accountInfo?.bankName) ||\n                !StringUtils.hasText(accountInfo?.accountNumber) ||\n                !StringUtils.hasText(accountInfo?.accountHolder)\n            ) {\n                throw EmptyAccountInfoException.EXCEPTION\n            }\n        }\n        // 유료티켓은 무조건 선착순 + 제휴 확인 + 1000원 이상\n        else if (payType == TicketPayType.PRICE_TICKET) {\n            if (type != TicketType.FIRST_COME_FIRST_SERVED) throw InvalidTicketTypeException.EXCEPTION\n            if (!isPartner) throw InvalidPartnerException.EXCEPTION\n            if (price?.isLessThan(Money.wons(MINIMUM_PAYMENT_WON)) == true) throw InvalidTicketPriceException.EXCEPTION\n        }\n        // 무료티켓은 무조건 0원\n        else {\n            if (price != Money.ZERO) throw InvalidTicketPriceException.EXCEPTION\n        }\n    }\n\n    /** 선착순 결제인지 확인하는 메서드 */\n    fun isFCFS(): Boolean = this.type?.isFCFS() ?: false\n\n    fun hasOption(): Boolean = itemOptionGroups.isNotEmpty()\n\n    fun getOptionGroupIds(): List<Long> =\n        itemOptionGroups.mapNotNull { it.optionGroup?.id }.sorted()\n\n    fun isQuantityReduced(): Boolean = quantity != supplyCount\n\n    fun reduceQuantity(quantity: Long?) {\n        val qty = quantity ?: 0L\n        if (this.quantity!! < 0) throw TicketItemQuantityException.EXCEPTION\n        validEnoughQuantity(qty)\n        this.quantity = this.quantity!! - qty\n    }\n\n    fun validEnoughQuantity(quantity: Long?) {\n        val qty = quantity ?: 0L\n        if (this.quantity!! < qty) throw TicketItemQuantityLackException.EXCEPTION\n    }\n\n    fun validPurchaseLimit(quantity: Long?) {\n        if (isPurchaseLimitExceed(quantity ?: 0L)) throw TicketPurchaseLimitException.EXCEPTION\n    }\n\n    fun isPurchaseLimitExceed(quantity: Long): Boolean = purchaseLimit!! < quantity\n\n    fun increaseQuantity(quantity: Long) {\n        if (this.quantity!! + quantity > supplyCount!!) throw TicketItemQuantityLargeException.EXCEPTION\n        this.quantity = this.quantity!! + quantity\n    }\n\n    fun isSold(): Boolean = quantity!! < supplyCount!!\n\n    fun isQuantityLeft(): Boolean = quantity!! > 0\n\n    /** 어드민 전용: 재고(quantity)와 공급량(supplyCount)을 동시에 조정 */\n    fun adminAdjustStock(delta: Long) {\n        val newQuantity = this.quantity!! + delta\n        val newSupplyCount = this.supplyCount!! + delta\n        if (newQuantity < 0) throw TicketItemQuantityException.EXCEPTION\n        if (newSupplyCount < 0) throw TicketItemQuantityException.EXCEPTION\n        this.quantity = newQuantity\n        this.supplyCount = newSupplyCount\n    }\n\n    /** 어드민 전용: 이벤트 상태 체크 없이 티켓 종류 정보 수정 */\n    fun adminUpdate(\n        name: String?,\n        description: String?,\n        price: Money?,\n        quantity: Long?,\n        purchaseLimit: Long?,\n    ) {\n        if (name != null) this.name = name\n        if (description != null) this.description = description\n        if (price != null) this.price = price\n        if (quantity != null) this.quantity = quantity\n        if (purchaseLimit != null) this.purchaseLimit = purchaseLimit\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/domain/TicketItemStatus.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.domain\n\nimport com.fasterxml.jackson.annotation.JsonValue\n\nenum class TicketItemStatus(\n    val value: String,\n    @JsonValue val kr: String,\n) {\n    // 유효\n    VALID(\"VALID\", \"유효\"),\n    // 삭제됨\n    DELETED(\"DELETED\", \"삭제됨\"),\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/domain/TicketPayType.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.domain\n\nimport com.fasterxml.jackson.annotation.JsonValue\n\nenum class TicketPayType(\n    val value: String,\n    @JsonValue val kr: String,\n) {\n    // 두둥티켓\n    DUDOONG_TICKET(\"DUDOONG_TICKET\", \"두둥티켓\"),\n    // 무료티켓\n    FREE_TICKET(\"FREE_TICKET\", \"무료티켓\"),\n    // 유료티켓\n    PRICE_TICKET(\"PRICE_TICKET\", \"유료티켓\"),\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/domain/TicketType.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.domain\n\nimport com.fasterxml.jackson.annotation.JsonValue\n\nenum class TicketType(\n    val value: String,\n    @JsonValue val kr: String,\n) {\n    // 선착순\n    FIRST_COME_FIRST_SERVED(\"FIRST_COME_FIRST_SERVED\", \"선착순\"),\n    // 승인\n    APPROVAL(\"APPROVAL\", \"승인\");\n\n    /** 선착순 방식인지 반환하는 메서드 */\n    fun isFCFS(): Boolean = this == FIRST_COME_FIRST_SERVED\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/DuplicatedItemOptionGroupException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass DuplicatedItemOptionGroupException private constructor() : DuDoongCodeException(TicketItemErrorCode.DUPLICATED_ITEM_OPTION_GROUP) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = DuplicatedItemOptionGroupException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/EmptyAccountInfoException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass EmptyAccountInfoException private constructor() : DuDoongCodeException(TicketItemErrorCode.EMPTY_ACCOUNT_INFO) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = EmptyAccountInfoException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/ForbiddenOptionChangeException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass ForbiddenOptionChangeException private constructor() : DuDoongCodeException(TicketItemErrorCode.FORBIDDEN_OPTION_CHANGE) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = ForbiddenOptionChangeException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/ForbiddenOptionGroupDeleteException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass ForbiddenOptionGroupDeleteException private constructor() : DuDoongCodeException(TicketItemErrorCode.FORBIDDEN_OPTION_GROUP_DELETE) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = ForbiddenOptionGroupDeleteException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/ForbiddenOptionPriceException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass ForbiddenOptionPriceException private constructor() : DuDoongCodeException(TicketItemErrorCode.FORBIDDEN_OPTION_PRICE) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = ForbiddenOptionPriceException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/ForbiddenTicketItemDeleteException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass ForbiddenTicketItemDeleteException private constructor() : DuDoongCodeException(TicketItemErrorCode.FORBIDDEN_TICKET_ITEM_DELETE) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = ForbiddenTicketItemDeleteException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/InvalidOptionGroupException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass InvalidOptionGroupException private constructor() : DuDoongCodeException(TicketItemErrorCode.INVALID_OPTION_GROUP) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = InvalidOptionGroupException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/InvalidOptionPriceException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass InvalidOptionPriceException private constructor() : DuDoongCodeException(TicketItemErrorCode.INVALID_OPTION_PRICE) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = InvalidOptionPriceException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/InvalidPartnerException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass InvalidPartnerException private constructor() : DuDoongCodeException(TicketItemErrorCode.INVALID_PARTNER) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = InvalidPartnerException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/InvalidTicketItemException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass InvalidTicketItemException private constructor() : DuDoongCodeException(TicketItemErrorCode.INVALID_TICKET_ITEM) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = InvalidTicketItemException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/InvalidTicketPriceException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass InvalidTicketPriceException private constructor() : DuDoongCodeException(TicketItemErrorCode.INVALID_TICKET_PRICE) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = InvalidTicketPriceException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/InvalidTicketTypeException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass InvalidTicketTypeException private constructor() : DuDoongCodeException(TicketItemErrorCode.INVALID_TICKET_TYPE) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = InvalidTicketTypeException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/NotAppliedItemOptionGroupException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass NotAppliedItemOptionGroupException private constructor() : DuDoongCodeException(TicketItemErrorCode.NOT_APPLIED_ITEM_OPTION_GROUP) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = NotAppliedItemOptionGroupException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/NotCorrectOptionAnswerException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass NotCorrectOptionAnswerException private constructor() : DuDoongCodeException(TicketItemErrorCode.OPTION_ANSWER_NOT_CORRECT) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = NotCorrectOptionAnswerException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/OptionGroupNotFoundException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass OptionGroupNotFoundException private constructor() : DuDoongCodeException(TicketItemErrorCode.OPTION_GROUP_NOT_FOUND) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = OptionGroupNotFoundException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/OptionNotFoundException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass OptionNotFoundException private constructor() : DuDoongCodeException(TicketItemErrorCode.OPTION_NOT_FOUND) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = OptionNotFoundException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/TicketItemErrorCode.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.consts.DuDoongStatic.BAD_REQUEST\nimport band.gosrock.common.consts.DuDoongStatic.NOT_FOUND\nimport band.gosrock.common.dto.ErrorReason\nimport band.gosrock.common.exception.BaseErrorCode\nimport java.lang.reflect.Field\n\nenum class TicketItemErrorCode(\n    private val status: Int,\n    private val code: String,\n    private val reason: String,\n) : BaseErrorCode {\n    @ExplainError(\"요청에서 보내준 티켓 상품 id 값이 올바르지 않을 때 발생하는 오류입니다.\")\n    TICKET_ITEM_NOT_FOUND(NOT_FOUND, \"Ticket_Item_404_1\", \"티켓 아이템을 찾을 수 없습니다.\"),\n\n    @ExplainError(\"요청에서 보내준 옵션 id 값이 올바르지 않을 때 발생하는 오류입니다.\")\n    OPTION_NOT_FOUND(NOT_FOUND, \"Option_404_1\", \"옵션을 찾을 수 없습니다.\"),\n\n    @ExplainError(\"주문 요청한 티켓 상품 재고가 부족할 때 발생하는 오류입니다.\")\n    TICKET_ITEM_QUANTITY_LACK(\n        BAD_REQUEST,\n        \"Ticket_Item_400_1\",\n        \"티켓 상품 재고가 부족합니다. ( 승인 대기 또는 앞선 주문으로 인해, 재고가 있어도 주문이 불가할 수 있습니다.)\"\n    ),\n\n    @ExplainError(\"주문 및 승인 요청 시 티켓 상품 재고보다 많은 양을 주문 시 발생하는 오류입니다.\")\n    TICKET_ITEM_QUANTITY_LESS_THAN_ZERO(BAD_REQUEST, \"Ticket_Item_400_2\", \"티켓 아이템 재고가 0보다 작을 수 없습니다.\"),\n\n    @ExplainError(\"설정할수 없는 티켓 가격일때 발생하는 오류입니다.\")\n    INVALID_TICKET_PRICE(BAD_REQUEST, \"Ticket_Item_400_3\", \"설정할 수 없는 티켓 가격입니다.\"),\n\n    @ExplainError(\"예매 취소 및 티켓 취소 요청 시 티켓 상품 공급량보다 많은 양이 반환될 때 발생하는 오류입니다.\")\n    TICKET_ITEM_QUANTITY_LARGER_THAN_SUPPLY_COUNT(BAD_REQUEST, \"Ticket_Item_400_4\", \"공급량보다 많은 티켓 아이템 재고가 설정되었습니다.\"),\n\n    @ExplainError(\"요청에서 보내준 옵션그룹 id 값이 올바르지 않을 때 발생하는 오류입니다.\")\n    OPTION_GROUP_NOT_FOUND(NOT_FOUND, \"Option_Group_404_1\", \"옵션그룹을 찾을 수 없습니다.\"),\n\n    @ExplainError(\"적용할 옵션이 해당 이벤트 소속이 아닐 때 발생하는 오류입니다.\")\n    INVALID_OPTION_GROUP(BAD_REQUEST, \"Option_Group_400_1\", \"해당 이벤트 소속 옵션그룹이 아닙니다.\"),\n\n    @ExplainError(\"옵션을 적용할 상품이 해당 이벤트 소속이 아닐 때 발생하는 오류입니다.\")\n    INVALID_TICKET_ITEM(BAD_REQUEST, \"Ticket_Item_400_5\", \"해당 이벤트 소속 티켓상품이 아닙니다.\"),\n\n    @ExplainError(\"해당 티켓상품에 이미 적용된 옵션일 경우 발생하는 오류입니다.\")\n    DUPLICATED_ITEM_OPTION_GROUP(BAD_REQUEST, \"Item_Option_Group_400_1\", \"이미 적용된 옵션입니다.\"),\n\n    OPTION_ANSWER_NOT_CORRECT(BAD_REQUEST, \"Option_400_1\", \"옵션에 대한 답변이 올바르지 않습니다. T/F형일 경우 예 아니요 로 보내주세요.\"),\n\n    TICKET_ITEM_PURCHASE_LIMIT(BAD_REQUEST, \"Ticket_Item_400_6\", \"해당 티켓상품 최대 구매 가능 갯수를 넘었습니다.\"),\n\n    @ExplainError(\"이미 재고가 감소되어 옵션 변경이 불가능할 경우 발생하는 오류입니다.\")\n    FORBIDDEN_OPTION_CHANGE(BAD_REQUEST, \"Item_Option_Group_400_2\", \"옵션 변경이 불가능한 상태입니다.\"),\n\n    @ExplainError(\"이미 재고가 감소되어 티켓상품 삭제가 불가능할 경우 발생하는 오류입니다.\")\n    FORBIDDEN_TICKET_ITEM_DELETE(BAD_REQUEST, \"Ticket_Item_400_7\", \"티켓상품 삭제가 불가능한 상태입니다.\"),\n\n    @ExplainError(\"이미 적용되어 옵션그룹 삭제가 불가능할 경우 발생하는 오류입니다.\")\n    FORBIDDEN_OPTION_GROUP_DELETE(BAD_REQUEST, \"Option_Group_400_2\", \"옵션그룹 삭제가 불가능한 상태입니다.\"),\n\n    @ExplainError(\"두둥티켓 타입에 계좌번호가 입력되지 않았을 경우 발생하는 오류입니다.\")\n    EMPTY_ACCOUNT_INFO(BAD_REQUEST, \"Ticket_Item_400_8\", \"계좌정보가 필요합니다.\"),\n\n    @ExplainError(\"티켓 지불방식과 승인방식이 불가능한 조합일때 발생하는 오류입니다.\")\n    INVALID_TICKET_TYPE(BAD_REQUEST, \"Ticket_Item_400_9\", \"잘못된 티켓 승인타입입니다.\"),\n\n    @ExplainError(\"제휴되지 않은 호스트가 유료티켓 생성을 요청했을때 발생하는 오류입니다.\")\n    INVALID_PARTNER(BAD_REQUEST, \"Ticket_Item_400_3\", \"제휴된 호스트가 아닙니다.\"),\n\n    @ExplainError(\"해당 티켓상품에 적용되지 않은 옵션을 취소 시도할 경우 발생하는 오류입니다.\")\n    NOT_APPLIED_ITEM_OPTION_GROUP(BAD_REQUEST, \"Item_Option_Group_400_3\", \"적용되지 않은 옵션입니다.\"),\n\n    @ExplainError(\"무료 티켓에 유료 옵션을 적용하려고 했을 때 발생하는 오류입니다.\")\n    FORBIDDEN_OPTION_PRICE(BAD_REQUEST, \"Item_Option_Group_400_4\", \"유료 옵션을 적용할 수 없습니다.\"),\n\n    @ExplainError(\"옵션 추가가격이 음수일 때 발생하는 오류입니다.\")\n    INVALID_OPTION_PRICE(BAD_REQUEST, \"Option_Group_400_3\", \"설정할 수 없는 추가 가격입니다.\");\n\n    override fun getErrorReason(): ErrorReason =\n        ErrorReason(status = status, code = code, reason = reason)\n\n    override fun getExplainError(): String {\n        val field: Field = this.javaClass.getField(this.name)\n        val annotation: ExplainError? = field.getAnnotation(ExplainError::class.java)\n        return annotation?.value ?: this.reason\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/TicketItemNotFoundException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass TicketItemNotFoundException private constructor() : DuDoongCodeException(TicketItemErrorCode.TICKET_ITEM_NOT_FOUND) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = TicketItemNotFoundException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/TicketItemQuantityException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass TicketItemQuantityException private constructor() : DuDoongCodeException(TicketItemErrorCode.TICKET_ITEM_QUANTITY_LESS_THAN_ZERO) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = TicketItemQuantityException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/TicketItemQuantityLackException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass TicketItemQuantityLackException private constructor() : DuDoongCodeException(TicketItemErrorCode.TICKET_ITEM_QUANTITY_LACK) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = TicketItemQuantityLackException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/TicketItemQuantityLargeException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass TicketItemQuantityLargeException private constructor() : DuDoongCodeException(TicketItemErrorCode.TICKET_ITEM_QUANTITY_LARGER_THAN_SUPPLY_COUNT) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = TicketItemQuantityLargeException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/exception/TicketPurchaseLimitException.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass TicketPurchaseLimitException private constructor() : DuDoongCodeException(TicketItemErrorCode.TICKET_ITEM_PURCHASE_LIMIT) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = TicketPurchaseLimitException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/repository/OptionGroupRepository.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.repository\n\nimport band.gosrock.domain.domains.ticket_item.domain.OptionGroup\nimport band.gosrock.domain.domains.ticket_item.domain.OptionGroupStatus\nimport org.springframework.data.jpa.repository.JpaRepository\nimport java.util.Optional\n\ninterface OptionGroupRepository : JpaRepository<OptionGroup, Long> {\n\n    fun findByIdAndOptionGroupStatus(optionGroupId: Long, status: OptionGroupStatus): Optional<OptionGroup>\n\n    fun findAllByEventIdAndOptionGroupStatus(eventId: Long, status: OptionGroupStatus): List<OptionGroup>\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/repository/OptionRepository.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.repository\n\nimport band.gosrock.domain.domains.ticket_item.domain.Option\nimport org.springframework.data.jpa.repository.JpaRepository\n\ninterface OptionRepository : JpaRepository<Option, Long> {\n\n    fun findAllByIdIn(ids: List<Long>): List<Option>\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/repository/TicketItemRepository.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.repository\n\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItemStatus\nimport org.springframework.data.jpa.repository.JpaRepository\nimport java.util.Optional\n\ninterface TicketItemRepository : JpaRepository<TicketItem, Long> {\n\n    fun findAllByEventIdAndTicketItemStatus(eventId: Long, status: TicketItemStatus): List<TicketItem>\n\n    fun existsByEventId(eventId: Long): Boolean\n\n    fun countByEventId(eventId: Long): Long\n\n    fun findByIdAndTicketItemStatus(ticketItemId: Long, status: TicketItemStatus): Optional<TicketItem>\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/service/ItemOptionGroupService.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.common.aop.redissonLock.RedissonLock\nimport band.gosrock.domain.domains.ticket_item.adaptor.OptionGroupAdaptor\nimport band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport band.gosrock.domain.domains.ticket_item.repository.TicketItemRepository\nimport org.springframework.transaction.annotation.Transactional\n\n@DomainService\n@Transactional(readOnly = true)\nclass ItemOptionGroupService(\n    private val ticketItemAdaptor: TicketItemAdaptor,\n    private val optionGroupAdaptor: OptionGroupAdaptor,\n    private val ticketItemRepository: TicketItemRepository\n) {\n\n    @RedissonLock(LockName = \"티켓관리\", identifier = \"ticketItemId\")\n    fun addItemOptionGroup(ticketItemId: Long, optionGroupId: Long, eventId: Long): TicketItem {\n        val ticketItem = ticketItemAdaptor.queryTicketItem(ticketItemId)\n        val optionGroup = optionGroupAdaptor.queryOptionGroup(optionGroupId)\n\n        // 해당 eventId에 속해 있는 티켓 아이템, 옵션그룹이 맞는지 확인\n        ticketItem.validateEventId(eventId)\n        optionGroup.validateEventId(eventId)\n\n        ticketItem.addItemOptionGroup(optionGroup)\n        return ticketItemRepository.save(ticketItem)\n    }\n\n    @RedissonLock(LockName = \"티켓관리\", identifier = \"ticketItemId\")\n    fun removeItemOptionGroup(ticketItemId: Long, optionGroupId: Long, eventId: Long): TicketItem {\n        val ticketItem = ticketItemAdaptor.queryTicketItem(ticketItemId)\n        val optionGroup = optionGroupAdaptor.queryOptionGroup(optionGroupId)\n\n        // 해당 eventId에 속해 있는 티켓 아이템, 옵션그룹이 맞는지 확인\n        ticketItem.validateEventId(eventId)\n        optionGroup.validateEventId(eventId)\n\n        ticketItem.removeItemOptionGroup(optionGroup)\n        return ticketItemRepository.save(ticketItem)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/service/TicketItemService.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.common.aop.redissonLock.RedissonLock\nimport band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport band.gosrock.domain.domains.ticket_item.exception.InvalidTicketItemException\nimport org.springframework.transaction.annotation.Transactional\n\n@DomainService\n@Transactional(readOnly = true)\nclass TicketItemService(\n    private val ticketItemAdaptor: TicketItemAdaptor\n) {\n\n    @Transactional\n    fun createTicketItem(ticketItem: TicketItem, isPartner: Boolean): TicketItem {\n        ticketItem.validateTicketPayType(isPartner)\n        return ticketItemAdaptor.save(ticketItem)\n    }\n\n    fun validateExistenceByEventId(eventId: Long) {\n        if (!ticketItemAdaptor.existsByEventId(eventId)) {\n            throw InvalidTicketItemException.EXCEPTION\n        }\n    }\n\n    @RedissonLock(LockName = \"티켓관리\", identifier = \"ticketItemId\")\n    fun softDeleteTicketItem(eventId: Long, ticketItemId: Long) {\n        val ticketItem = ticketItemAdaptor.queryTicketItem(ticketItemId)\n        // 해당 eventId에 속해 있는 티켓 아이템이 맞는지 확인\n        ticketItem.validateEventId(eventId)\n        ticketItem.softDeleteTicketItem()\n        ticketItemAdaptor.save(ticketItem)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/ticket_item/service/TicketOptionService.kt",
    "content": "package band.gosrock.domain.domains.ticket_item.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.domains.ticket_item.adaptor.OptionGroupAdaptor\nimport band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor\nimport band.gosrock.domain.domains.ticket_item.domain.OptionGroup\nimport org.springframework.transaction.annotation.Transactional\n\n@DomainService\n@Transactional(readOnly = true)\nclass TicketOptionService(\n    private val optionGroupAdaptor: OptionGroupAdaptor,\n    private val ticketItemAdaptor: TicketItemAdaptor\n) {\n\n    @Transactional\n    fun createTicketOption(optionGroup: OptionGroup): OptionGroup {\n        return optionGroupAdaptor.save(optionGroup)\n    }\n\n    @Transactional\n    fun softDeleteOptionGroup(eventId: Long, optionGroupId: Long) {\n        val optionGroup = optionGroupAdaptor.queryOptionGroup(optionGroupId)\n        // 해당 eventId에 속해 있는 옵션그룹이 맞는지 확인\n        optionGroup.validateEventId(eventId)\n\n        val ticketItems = ticketItemAdaptor.findAllByEventId(eventId)\n        optionGroup.softDeleteOptionGroup(ticketItems)\n        optionGroupAdaptor.save(optionGroup)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/adaptor/RefreshTokenAdaptor.kt",
    "content": "package band.gosrock.domain.domains.user.adaptor\n\nimport band.gosrock.common.annotation.Adaptor\nimport band.gosrock.common.exception.RefreshTokenExpiredException\nimport band.gosrock.domain.domains.user.domain.RefreshTokenEntity\nimport band.gosrock.domain.domains.user.repository.RefreshTokenRepository\n\n@Adaptor\nclass RefreshTokenAdaptor(private val refreshTokenRepository: RefreshTokenRepository) {\n\n    fun queryRefreshToken(refreshToken: String): RefreshTokenEntity =\n        refreshTokenRepository.findByRefreshToken(refreshToken)\n            .orElseThrow { RefreshTokenExpiredException.EXCEPTION }\n\n    fun save(refreshToken: RefreshTokenEntity): RefreshTokenEntity =\n        refreshTokenRepository.save(refreshToken)\n\n    fun deleteByUserId(userId: Long) {\n        refreshTokenRepository.deleteById(userId.toString())\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/adaptor/UserAdaptor.kt",
    "content": "package band.gosrock.domain.domains.user.adaptor\n\nimport band.gosrock.common.annotation.Adaptor\nimport band.gosrock.domain.domains.user.domain.AccountState\nimport band.gosrock.domain.domains.user.domain.OauthInfo\nimport band.gosrock.domain.domains.user.domain.User\nimport band.gosrock.domain.domains.user.exception.UserNotFoundException\nimport band.gosrock.domain.domains.user.repository.UserRepository\nimport java.time.LocalDateTime\n\n@Adaptor\nclass UserAdaptor(private val userRepository: UserRepository) {\n\n    fun queryUser(userId: Long?): User =\n        userRepository.findById(userId ?: throw UserNotFoundException.EXCEPTION).orElseThrow { UserNotFoundException.EXCEPTION }\n\n    fun exist(oauthInfo: OauthInfo): Boolean =\n        userRepository.findByOauthInfo(oauthInfo).isPresent\n\n    fun queryUserByOauthInfo(oauthInfo: OauthInfo): User =\n        userRepository.findByOauthInfo(oauthInfo).orElseThrow { UserNotFoundException.EXCEPTION }\n\n    /** user id 리스트에 포함되어 있는 유저를 모두 가져오는 쿼리 */\n    fun queryUserListByIdIn(userIdList: List<Long>): List<User> =\n        userRepository.findAllByIdIn(userIdList)\n\n    /** 이메일로 유저를 가져오는 쿼리 */\n    fun queryUserByEmail(email: String): User =\n        userRepository.findByProfileEmailAndAccountState(email, AccountState.NORMAL)\n            .orElseThrow { UserNotFoundException.EXCEPTION }\n\n    fun countNormalUserCreatedBefore(before: LocalDateTime): Long =\n        userRepository.countByAccountStateAndCreatedAtBefore(AccountState.NORMAL, before)\n\n    fun findUserByIdIn(userIds: List<Long>): List<User> =\n        userRepository.findByIdIn(userIds)\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/domain/AccountRole.kt",
    "content": "package band.gosrock.domain.domains.user.domain\n\nenum class AccountRole(val value: String) {\n    USER(\"USER\"),\n    MANAGER(\"MANAGER\"),\n    ADMIN(\"ADMIN\"),\n    SUPER_ADMIN(\"SUPER_ADMIN\"),\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/domain/AccountState.kt",
    "content": "package band.gosrock.domain.domains.user.domain\n\nenum class AccountState(val value: String) {\n    NORMAL(\"NORMAL\"),\n    // 탈퇴한유저\n    DELETED(\"DELETED\"),\n    // 영구정지\n    FORBIDDEN(\"FORBIDDEN\"),\n    // 7일정지등..? 나중을위한\n    SUSPENDED(\"SUSPENDED\"),\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/domain/OauthInfo.kt",
    "content": "package band.gosrock.domain.domains.user.domain\n\nimport band.gosrock.common.consts.DuDoongStatic\nimport java.time.LocalDateTime\nimport jakarta.persistence.Embeddable\nimport jakarta.persistence.EnumType\nimport jakarta.persistence.Enumerated\n\n@Embeddable\nclass OauthInfo(\n    @Enumerated(EnumType.STRING)\n    var provider: OauthProvider? = null,\n    var oid: String? = null,\n) {\n    fun withDrawOauthInfo(): OauthInfo =\n        OauthInfo(provider!!, DuDoongStatic.WITHDRAW_PREFIX + LocalDateTime.now() + \":\" + oid)\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/domain/OauthProvider.kt",
    "content": "package band.gosrock.domain.domains.user.domain\n\nenum class OauthProvider(val value: String) {\n    KAKAO(\"KAKAO\"),\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/domain/Profile.kt",
    "content": "package band.gosrock.domain.domains.user.domain\n\nimport band.gosrock.domain.common.vo.ImageVo\nimport band.gosrock.domain.common.vo.PhoneNumberVo\nimport jakarta.persistence.Embeddable\nimport jakarta.persistence.Embedded\n\n@Embeddable\nclass Profile(\n    var name: String? = null,\n    var email: String? = null,\n    phoneNumber: String? = null,\n    profileImage: String? = null,\n) {\n    @Embedded\n    var phoneNumberVo: PhoneNumberVo? = PhoneNumberVo.valueOf(phoneNumber)\n\n    @Embedded\n    var profileImage: ImageVo? = ImageVo.valueOf(profileImage)\n\n    fun changeName(newName: String) {\n        require(newName.isNotBlank()) { \"이름은 빈 값일 수 없습니다.\" }\n        require(newName.length in 2..7) { \"이름은 2~7자여야 합니다.\" }\n        this.name = newName\n    }\n\n    fun withdraw() {\n        this.name = \"탈퇴한 유저\"\n        this.email = null\n        this.phoneNumberVo = null\n        this.profileImage = null\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/domain/RefreshTokenEntity.kt",
    "content": "package band.gosrock.domain.domains.user.domain\n\nimport org.springframework.data.annotation.Id\nimport org.springframework.data.redis.core.RedisHash\nimport org.springframework.data.redis.core.TimeToLive\nimport org.springframework.data.redis.core.index.Indexed\n\n@RedisHash(value = \"refreshToken\")\nclass RefreshTokenEntity(\n    @Id var id: Long? = null,\n    @Indexed var refreshToken: String? = null,\n    @TimeToLive var ttl: Long? = null,\n) {\n    fun updateTTL(ttl: Long) {\n        this.ttl = (this.ttl ?: 0) + ttl\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/domain/User.kt",
    "content": "package band.gosrock.domain.domains.user.domain\n\nimport band.gosrock.domain.common.aop.domainEvent.Events\nimport band.gosrock.domain.common.events.user.UserRegisterEvent\nimport band.gosrock.domain.common.model.BaseTimeEntity\nimport band.gosrock.domain.common.vo.UserInfoVo\nimport band.gosrock.domain.common.vo.UserProfileVo\nimport band.gosrock.domain.domains.user.exception.AlreadyDeletedUserException\nimport band.gosrock.domain.domains.user.exception.EmptyPhoneNumException\nimport band.gosrock.domain.domains.user.exception.ForbiddenUserException\nimport band.gosrock.infrastructure.config.alilmTalk.dto.AlimTalkUserInfo\nimport band.gosrock.infrastructure.config.mail.dto.EmailUserInfo\nimport com.google.i18n.phonenumbers.NumberParseException\nimport java.time.LocalDateTime\nimport jakarta.persistence.Column\nimport jakarta.persistence.Embedded\nimport jakarta.persistence.Entity\nimport jakarta.persistence.EnumType\nimport jakarta.persistence.Enumerated\nimport jakarta.persistence.GeneratedValue\nimport jakarta.persistence.GenerationType\nimport jakarta.persistence.Id\nimport jakarta.persistence.PostPersist\nimport jakarta.persistence.Table\nimport jakarta.persistence.UniqueConstraint\n\n@Entity\n@Table(\n    name = \"tbl_user\",\n    uniqueConstraints = [UniqueConstraint(columnNames = [\"oid\", \"provider\"])],\n)\nclass User(\n    @Embedded\n    var profile: Profile? = null,\n\n    @Embedded\n    var oauthInfo: OauthInfo? = null,\n\n    // 마케팅 동의 여부\n    var marketingAgree: Boolean = false,\n) : BaseTimeEntity() {\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    @Column(name = \"user_id\")\n    var id: Long? = null\n        protected set\n\n    @Enumerated(EnumType.STRING)\n    var accountState: AccountState = AccountState.NORMAL\n        protected set\n\n    @Enumerated(EnumType.STRING)\n    var accountRole: AccountRole = AccountRole.USER\n        protected set\n\n    // 이메일 수신 여부\n    var receiveMail: Boolean = true\n        protected set\n\n    var lastLoginAt: LocalDateTime = LocalDateTime.now()\n        protected set\n\n    @PostPersist\n    fun registerEvent() {\n        val event = UserRegisterEvent(userId = id)\n        Events.raise(event)\n    }\n\n    fun changeProfile(newProfile: Profile) {\n        profile = newProfile\n    }\n\n    fun changeName(newName: String) {\n        profile?.changeName(newName)\n    }\n\n    fun withDrawUser() {\n        if (accountState == AccountState.DELETED) throw AlreadyDeletedUserException.EXCEPTION\n        accountState = AccountState.DELETED\n        profile?.withdraw()\n        oauthInfo = oauthInfo?.withDrawOauthInfo()\n        marketingAgree = false\n        receiveMail = false\n    }\n\n    fun login() {\n        if (accountState != AccountState.NORMAL) throw ForbiddenUserException.EXCEPTION\n        lastLoginAt = LocalDateTime.now()\n    }\n\n    fun toUserInfoVo(): UserInfoVo = UserInfoVo.from(this)\n    fun toUserProfileVo(): UserProfileVo = UserProfileVo.from(this)\n\n    fun toEmailUserInfo(): EmailUserInfo =\n        EmailUserInfo(profile!!.name ?: \"\", profile!!.email ?: \"\", receiveMail)\n\n    @Throws(NumberParseException::class)\n    fun toAlimTalkUserInfo(): AlimTalkUserInfo {\n        if (profile?.phoneNumberVo == null) throw EmptyPhoneNumException.EXCEPTION\n        return AlimTalkUserInfo(profile!!.name ?: \"\", profile!!.phoneNumberVo!!.getNaverSmsToNumber())\n    }\n\n    fun isReceiveEmail(): Boolean = receiveMail\n    fun isAgreeMarketing(): Boolean = marketingAgree\n\n    fun toggleReceiveEmail() {\n        receiveMail = !receiveMail\n    }\n\n    fun toggleMarketingAgree() {\n        marketingAgree = !marketingAgree\n    }\n\n    fun changeRole(newRole: AccountRole) {\n        accountRole = newRole\n    }\n\n    fun changeAccountState(newState: AccountState) {\n        if (newState == AccountState.DELETED) {\n            withDrawUser()\n            return\n        }\n        accountState = newState\n    }\n\n    fun isDeletedUser(): Boolean = accountState == AccountState.DELETED\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/exception/AlreadyDeletedUserException.kt",
    "content": "package band.gosrock.domain.domains.user.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass AlreadyDeletedUserException private constructor() : DuDoongCodeException(UserErrorCode.USER_ALREADY_DELETED) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = AlreadyDeletedUserException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/exception/AlreadySignUpUserException.kt",
    "content": "package band.gosrock.domain.domains.user.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass AlreadySignUpUserException private constructor() : DuDoongCodeException(UserErrorCode.USER_ALREADY_SIGNUP) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = AlreadySignUpUserException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/exception/EmptyPhoneNumException.kt",
    "content": "package band.gosrock.domain.domains.user.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass EmptyPhoneNumException private constructor() : DuDoongCodeException(UserErrorCode.USER_PHONE_EMPTY) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = EmptyPhoneNumException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/exception/ForbiddenUserException.kt",
    "content": "package band.gosrock.domain.domains.user.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass ForbiddenUserException private constructor() : DuDoongCodeException(UserErrorCode.USER_FORBIDDEN) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = ForbiddenUserException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/exception/UserErrorCode.kt",
    "content": "package band.gosrock.domain.domains.user.exception\n\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.consts.DuDoongStatic.BAD_REQUEST\nimport band.gosrock.common.consts.DuDoongStatic.FORBIDDEN\nimport band.gosrock.common.consts.DuDoongStatic.NOT_FOUND\nimport band.gosrock.common.dto.ErrorReason\nimport band.gosrock.common.exception.BaseErrorCode\nimport java.lang.reflect.Field\n\nenum class UserErrorCode(\n    private val status: Int,\n    private val code: String,\n    private val reason: String,\n) : BaseErrorCode {\n    @ExplainError(\"회원가입시에 이미 회원가입한 유저일시 발생하는 오류. 회원가입전엔 항상 register valid check 를 해주세요\")\n    USER_ALREADY_SIGNUP(BAD_REQUEST, \"USER_400_1\", \"이미 회원가입한 유저입니다.\"),\n\n    @ExplainError(\"정지 처리된 유저일 경우 밣생하는 오류\")\n    USER_FORBIDDEN(FORBIDDEN, \"USER_403_1\", \"접근이 제한된 유저입니다.\"),\n\n    @ExplainError(\"탈퇴한 유저로 접근하려는 경우\")\n    USER_ALREADY_DELETED(FORBIDDEN, \"USER_403_2\", \"이미 지워진 유저입니다.\"),\n\n    @ExplainError(\"유저 정보를 찾을 수 없는 경우\")\n    USER_NOT_FOUND(NOT_FOUND, \"USER_404_1\", \"유저 정보를 찾을 수 없습니다.\"),\n    USER_PHONE_INVALID(BAD_REQUEST, \"USER_400_2\", \"유저의 휴대폰 전화번호가 올바르지않습니다. 두둥 관리자에게 문의주세요\"),\n\n    @ExplainError(\"알림톡 발송시 보내는 유저의 전화번호 정보가 null이라 알림톡 발송 불가 경우\")\n    USER_PHONE_EMPTY(BAD_REQUEST, \"USER_400_3\", \"유저의 휴대폰 전화번호가 null입니다.\");\n\n    override fun getErrorReason(): ErrorReason =\n        ErrorReason(status = status, code = code, reason = reason)\n\n    override fun getExplainError(): String {\n        val field: Field = this.javaClass.getField(this.name)\n        val annotation = field.getAnnotation(ExplainError::class.java)\n        return annotation?.value ?: reason\n    }\n\n    fun getReason(): String = reason\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/exception/UserNotFoundException.kt",
    "content": "package band.gosrock.domain.domains.user.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass UserNotFoundException private constructor() : DuDoongCodeException(UserErrorCode.USER_NOT_FOUND) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = UserNotFoundException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/exception/UserPhoneNumberInvalidException.kt",
    "content": "package band.gosrock.domain.domains.user.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\n\nclass UserPhoneNumberInvalidException private constructor() : DuDoongCodeException(UserErrorCode.USER_PHONE_INVALID) {\n    companion object {\n        @JvmField\n        val EXCEPTION: DuDoongCodeException = UserPhoneNumberInvalidException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/repository/RefreshTokenRepository.kt",
    "content": "package band.gosrock.domain.domains.user.repository\n\nimport band.gosrock.domain.domains.user.domain.RefreshTokenEntity\nimport org.springframework.data.repository.CrudRepository\nimport java.util.Optional\n\ninterface RefreshTokenRepository : CrudRepository<RefreshTokenEntity, String> {\n    fun findByRefreshToken(refreshToken: String): Optional<RefreshTokenEntity>\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/repository/UserRepository.kt",
    "content": "package band.gosrock.domain.domains.user.repository\n\nimport band.gosrock.domain.domains.user.domain.AccountState\nimport band.gosrock.domain.domains.user.domain.OauthInfo\nimport band.gosrock.domain.domains.user.domain.User\nimport org.springframework.data.domain.Page\nimport org.springframework.data.domain.Pageable\nimport org.springframework.data.jpa.repository.JpaRepository\nimport org.springframework.data.jpa.repository.Query\nimport org.springframework.data.repository.query.Param\nimport java.time.LocalDateTime\nimport java.util.Optional\n\ninterface UserRepository : JpaRepository<User, Long> {\n    fun findByOauthInfo(oauthInfo: OauthInfo): Optional<User>\n\n    /** user id 리스트에 포함되어 있는 유저를 모두 가져오는 쿼리 */\n    fun findAllByIdIn(id: List<Long>): List<User>\n\n    /** email 로 유저를 가져오는 쿼리 */\n    fun findByProfileEmailAndAccountState(email: String, accountState: AccountState): Optional<User>\n\n    fun countByAccountStateAndCreatedAtBefore(accountState: AccountState, before: LocalDateTime): Long\n\n    fun findByIdIn(userIds: List<Long>): List<User>\n\n    /** Admin: 키워드로 유저 검색 (이름 또는 이메일) */\n    @Query(\n        \"SELECT u FROM User u WHERE \" +\n            \"(:keyword IS NULL OR u.profile.name LIKE %:keyword% OR u.profile.email LIKE %:keyword%)\"\n    )\n    fun findAllByKeyword(@Param(\"keyword\") keyword: String?, pageable: Pageable): Page<User>\n\n    /** Admin: 페이지네이션 없이 전체 유저 조회 (엑셀 다운로드용) */\n    @Query(\n        \"SELECT u FROM User u WHERE \" +\n            \"(:keyword IS NULL OR u.profile.name LIKE %:keyword% OR u.profile.email LIKE %:keyword%)\"\n    )\n    fun findAllByKeywordNoPage(@Param(\"keyword\") keyword: String?): List<User>\n\n    /** Admin: 오늘 가입한 유저 수 */\n    fun countByCreatedAtAfter(after: LocalDateTime): Long\n\n    /** Admin: 기간 내 가입한 유저 수 */\n    fun countByCreatedAtBetween(start: LocalDateTime, end: LocalDateTime): Long\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/kotlin/band/gosrock/domain/domains/user/service/UserDomainService.kt",
    "content": "package band.gosrock.domain.domains.user.service\n\nimport band.gosrock.common.annotation.DomainService\nimport band.gosrock.domain.common.aop.redissonLock.RedissonLock\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor\nimport band.gosrock.domain.domains.user.domain.OauthInfo\nimport band.gosrock.domain.domains.user.domain.Profile\nimport band.gosrock.domain.domains.user.domain.User\nimport band.gosrock.domain.domains.user.exception.AlreadySignUpUserException\nimport band.gosrock.domain.domains.user.repository.UserRepository\nimport org.springframework.transaction.annotation.Transactional\n\n@DomainService\nopen class UserDomainService(\n    private val userRepository: UserRepository,\n    private val userAdaptor: UserAdaptor,\n) {\n    @Transactional\n    @RedissonLock(LockName = \"유저등록\", identifier = \"oid\", paramClassType = OauthInfo::class)\n    open fun registerUser(profile: Profile, oauthInfo: OauthInfo, marketingAgree: Boolean): User {\n        validUserCanRegister(oauthInfo)\n        val newUser = User(\n            profile = profile,\n            marketingAgree = marketingAgree,\n            oauthInfo = oauthInfo,\n        )\n        userRepository.save(newUser)\n        return newUser\n    }\n\n    @Transactional\n    @RedissonLock(LockName = \"개발용회원가입\", identifier = \"oid\", paramClassType = OauthInfo::class)\n    open fun upsertUser(profile: Profile, oauthInfo: OauthInfo): User =\n        userRepository.findByOauthInfo(oauthInfo).orElseGet {\n            val newUser = User(\n                profile = profile,\n                marketingAgree = true,\n                oauthInfo = oauthInfo,\n            )\n            userRepository.save(newUser)\n            newUser\n        }\n\n    fun checkUserCanRegister(oauthInfo: OauthInfo): Boolean = !userAdaptor.exist(oauthInfo)\n\n    fun validUserCanRegister(oauthInfo: OauthInfo) {\n        if (!checkUserCanRegister(oauthInfo)) throw AlreadySignUpUserException.EXCEPTION\n    }\n\n    @Transactional\n    open fun loginUser(oauthInfo: OauthInfo): User {\n        val user = userAdaptor.queryUserByOauthInfo(oauthInfo)\n        user.login()\n        return user\n    }\n\n    @Transactional\n    @RedissonLock(LockName = \"유저탈퇴\", identifier = \"userId\")\n    open fun withDrawUser(userId: Long) {\n        val user = userAdaptor.queryUser(userId)\n        user.withDrawUser()\n    }\n\n    @Transactional\n    open fun toggleMarketAgree(currentUserId: Long) {\n        val user = userAdaptor.queryUser(currentUserId)\n        user.toggleMarketingAgree()\n    }\n\n    @Transactional\n    open fun toggleMailAgree(currentUserId: Long) {\n        val user = userAdaptor.queryUser(currentUserId)\n        user.toggleReceiveEmail()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/main/resources/application-domain-local.yml",
    "content": "spring:\n  datasource:\n    url: jdbc:mysql://127.0.0.1:13306/dudoong?useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&tinyInt1isBit=false\n    driver-class-name: com.mysql.cj.jdbc.Driver\n    hikari:\n      maxLifetime: 580000\n      maximum-pool-size: 10\n    username: dudoong\n    password: dudoong\n  jpa:\n    show-sql: true\n    properties:\n      hibernate:\n        format_sql: true\n        default_batch_fetch_size: 100\n    hibernate:\n      ddl-auto: update\n    defer-datasource-initialization: true\n    sql:\n      init:\n        mode: always\n    open-in-view: false\n    database-platform: org.hibernate.dialect.MySQLDialect\nlogging:\n  level:\n    com.zaxxer.hikari.HikariConfig: DEBUG\n    com.zaxxer.hikari: TRACE\n    org.springframework.orm.jpa: DEBUG\n    org.springframework.transaction: DEBUG\n"
  },
  {
    "path": "DuDoong-Domain/src/main/resources/application-domain.yml",
    "content": "---\nspring:\n  config:\n    activate:\n      on-profile: dev\n  datasource:\n    url: jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${DB_NAME}?useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&tinyInt1isBit=false\n    driver-class-name: com.mysql.cj.jdbc.Driver\n    hikari:\n      maxLifetime: 580000\n      maximum-pool-size: 10\n    password: ${MYSQL_PASSWORD}\n    username: ${MYSQL_USERNAME}\n  jpa:\n    show-sql: ${SHOW_SQL:true}\n    properties:\n      hibernate:\n        format_sql: ${FORMAT_SQL:true}\n        default_batch_fetch_size: ${JPA_BATCH_FETCH_SIZE:100}\n    hibernate:\n      ddl-auto: update\n    defer-datasource-initialization: true\n    sql:\n      init:\n        mode: always\n    open-in-view: false\n    database-platform: org.hibernate.dialect.MySQLDialect\nlogging:\n  level:\n    com.zaxxer.hikari.HikariConfig: DEBUG\n    com.zaxxer.hikari: TRACE\n    org.springframework.orm.jpa: DEBUG\n    org.springframework.transaction: DEBUG\n---\nspring:\n  config:\n    activate:\n      on-profile: test\n  datasource:\n    url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false;MODE=MYSQL\n---\nspring:\n  config:\n    activate:\n      on-profile: staging\n  datasource:\n    url: jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${DB_NAME}?useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&tinyInt1isBit=false\n    driver-class-name: com.mysql.cj.jdbc.Driver\n    hikari:\n      maxLifetime: 580000\n      maximum-pool-size: 20\n    password: ${MYSQL_PASSWORD}\n    username: ${MYSQL_USERNAME}\n  jpa:\n    properties:\n      hibernate:\n        default_batch_fetch_size: ${JPA_BATCH_FETCH_SIZE:100}\n    hibernate:\n      ddl-auto: none\n    database-platform: org.hibernate.dialect.MySQLDialect\nlogging:\n  level:\n    org.springframework.orm.jpa: DEBUG\n    org.springframework.transaction: DEBUG\n---\nspring:\n  config:\n    activate:\n      on-profile: prod\n  datasource:\n    url: jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${DB_NAME}?useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&tinyInt1isBit=false\n    driver-class-name: com.mysql.cj.jdbc.Driver\n    hikari:\n      maxLifetime: 580000\n      maximum-pool-size: 20\n    password: ${MYSQL_PASSWORD}\n    username: ${MYSQL_USERNAME}\n  jpa:\n    properties:\n      hibernate:\n        default_batch_fetch_size: ${JPA_BATCH_FETCH_SIZE:100}\n    hibernate:\n      ddl-auto: none\n    database-platform: org.hibernate.dialect.MySQLDialect\n\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/CunCurrencyExecutorService.java",
    "content": "package band.gosrock.domain;\n\n\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.function.Executable;\n\n/** 동시성 테스트를 편하게 하라고 친철히 만든.. 동시성용 util 클래스 - 이찬진 */\n@Slf4j\npublic class CunCurrencyExecutorService {\n    static int numberOfThreads = 10;\n    static int numberOfThreadPool = 5;\n\n    public static void execute(Executable executable, AtomicLong successCount)\n            throws InterruptedException {\n        ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreadPool);\n        CountDownLatch latch = new CountDownLatch(numberOfThreads);\n\n        for (long i = 1; i <= numberOfThreads; i++) {\n            executorService.submit(\n                    () -> {\n                        try {\n                            executable.execute();\n                            // 오류없이 성공을 하면 성공횟수를 증가시킵니다.\n                            successCount.getAndIncrement();\n                        } catch (Throwable e) {\n                            // 에러뜨면 여기서 확인해보셔요!\n                            log.info(e.getClass().getName());\n                        } finally {\n                            latch.countDown();\n                        }\n                    });\n        }\n        latch.await();\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/DisableDomainEvent.java",
    "content": "package band.gosrock.domain;\n\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport org.springframework.test.context.TestPropertySource;\n\n/** 도메인 이벤트의 발행을 중지 시킬 수 있습니다. -이찬진 */\n@Target({ElementType.TYPE})\n@Retention(RetentionPolicy.RUNTIME)\n@TestPropertySource(properties = {\"ableDomainEvent=false\"})\n@Documented\npublic @interface DisableDomainEvent {}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/DisableRedissonLock.java",
    "content": "package band.gosrock.domain;\n\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport org.springframework.test.context.TestPropertySource;\n\n/** redisson 분산락 Aop 를 중지 시킬 수 있습니다. 테스트 클래스에서 락이 없을때의 실패 테스트를 진행하기 위함. -이찬진 */\n@Target({ElementType.TYPE})\n@Retention(RetentionPolicy.RUNTIME)\n@TestPropertySource(properties = {\"ableRedissonLock=false\"})\n@Documented\npublic @interface DisableRedissonLock {}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/DomainIntegrateProfileResolver.java",
    "content": "package band.gosrock.domain;\n\n\nimport org.springframework.test.context.ActiveProfilesResolver;\n\n/**\n * activeProfile 의 Resolver 를 지정 통합테스트에 필요한 properties 인 common, infrastructure , domain 을 지정하기 위함.\n */\npublic class DomainIntegrateProfileResolver implements ActiveProfilesResolver {\n\n    @Override\n    public String[] resolve(Class<?> testClass) {\n        // some code to find out your active profiles\n        return new String[] {\"common\", \"infrastructure\", \"domain\"};\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/DomainIntegrateSpringBootTest.java",
    "content": "package band.gosrock.domain;\n\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.ActiveProfiles;\n\n/** 도메인 모듈의 통합테스트의 편의성을 위해서 만든 어노테이션 -이찬진 */\n@Target({ElementType.TYPE})\n@Retention(RetentionPolicy.RUNTIME)\n@SpringBootTest(classes = DomainIntegrateTestConfig.class)\n@ActiveProfiles(resolver = DomainIntegrateProfileResolver.class)\n@Documented\npublic @interface DomainIntegrateSpringBootTest {}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/DomainIntegrateTestConfig.java",
    "content": "package band.gosrock.domain;\n\n\nimport band.gosrock.common.DuDoongCommonApplication;\nimport band.gosrock.infrastructure.DuDoongInfraApplication;\nimport org.springframework.context.annotation.ComponentScan;\nimport org.springframework.context.annotation.Configuration;\n\n/** 스프링 부트 설정의 컴포넌트 스캔범위를 지정 통합 테스트를 위함 */\n@Configuration\n@ComponentScan(\n        basePackageClasses = {\n            DuDoongInfraApplication.class,\n            DuDoongDomainApplication.class,\n            DuDoongCommonApplication.class\n        })\npublic class DomainIntegrateTestConfig {}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/common/aop/redissonLock/RedissonLockAopTest.java",
    "content": "package band.gosrock.domain.common.aop.redissonLock;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport band.gosrock.common.exception.BadLockIdentifierException;\nimport band.gosrock.domain.common.dto.ProfileViewDto;\nimport java.lang.reflect.InvocationTargetException;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.redisson.api.RedissonClient;\n\n@ExtendWith(MockitoExtension.class)\nclass RedissonLockAopTest {\n    @Mock RedissonClient redissonClient;\n    @Mock CallTransactionFactory callTransactionFactory;\n\n    RedissonLockAop redissonLockAop;\n\n    @BeforeEach\n    public void beforeEach() {\n        redissonLockAop = new RedissonLockAop(redissonClient, callTransactionFactory);\n    }\n\n    @Test\n    public void 커스텀오브젝트에서_클래스타입과_식별자로_키를_가져와야한다()\n            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {\n        ProfileViewDto profileViewDto = new ProfileViewDto(1000L, null, null);\n        String otherParam = \"param\";\n\n        Object[] testArgs = {profileViewDto, otherParam};\n        String[] parameterNames = {\"profileViewDto\", \"otherParam\"};\n        Class<?> paramClassType = profileViewDto.getClass();\n        String dynamicKey =\n                redissonLockAop.createDynamicKeyFromObject(testArgs, paramClassType, \"id\");\n        assertEquals(\"1000\", dynamicKey);\n    }\n\n    @Test\n    public void 기본오브젝트에서_식별자로_키를_가져와야한다() {\n        ProfileViewDto profileViewDto = new ProfileViewDto(1000L, null, null);\n        String keyParamName = \"다이내믹키가될값\";\n\n        Object[] testArgs = {profileViewDto, keyParamName};\n        String[] parameterNames = {\"파라미터첫번째\", \"파라미터두번째\"};\n        Class<?> paramClassType = profileViewDto.getClass();\n\n        String dynamicKey =\n                redissonLockAop.createDynamicKeyFromPrimitive(parameterNames, testArgs, \"파라미터두번째\");\n        assertEquals(\"다이내믹키가될값\", dynamicKey);\n    }\n\n    @Test\n    public void 잘못된_인자를_설정하면_오류가_발생해야한다() {\n        ProfileViewDto profileViewDto = new ProfileViewDto(1000L, null, null);\n        // 키값\n        String keyParamName = \"다른값\";\n        // 실제 인자로 넘겨질 값들\n        Object[] testArgs = {profileViewDto, keyParamName};\n        // 파라미터 이름\n        String[] parameterNames = {\"profileViewDto\", \"otherParam\"};\n        // 클래스 타입\n        Class<?> paramClassType = profileViewDto.getClass();\n\n        assertThrows(\n                BadLockIdentifierException.class,\n                () -> {\n                    redissonLockAop.generateDynamicKey(\n                            \"이상한식별자\", testArgs, paramClassType, parameterNames);\n                });\n\n        assertThrows(\n                BadLockIdentifierException.class,\n                () -> {\n                    redissonLockAop.generateDynamicKey(\n                            \"이상한식별자\", testArgs, Object.class, parameterNames);\n                });\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/common/vo/RefundInfoVoTest.java",
    "content": "package band.gosrock.domain.common.vo;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport java.time.LocalDateTime;\nimport org.junit.jupiter.api.Test;\n\nclass RefundInfoVoTest {\n\n    @Test\n    void 환불가능_시간_테스() {\n        // given\n        LocalDateTime endAt = LocalDateTime.MAX;\n        RefundInfoVo refundInfoVo = RefundInfoVo.from(endAt);\n\n        // when\n        Boolean availAble = refundInfoVo.getAvailAble();\n        // then\n        assertTrue(availAble);\n    }\n\n    @Test\n    void 환불불가_시간_테스트() {\n        // given\n        LocalDateTime endAt = LocalDateTime.MIN;\n        RefundInfoVo refundInfoVo = RefundInfoVo.from(endAt);\n\n        // when\n        Boolean availAble = refundInfoVo.getAvailAble();\n        // then\n        assertFalse(availAble);\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/cart/domain/CartLineItemTest.java",
    "content": "package band.gosrock.domain.domains.cart.domain;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.BDDMockito.given;\n\nimport band.gosrock.domain.common.vo.Money;\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem;\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\nclass CartLineItemTest {\n\n    @Mock CartOptionAnswer cartOptionAnswer;\n\n    @Mock TicketItem hasPriceItem;\n\n    @Mock TicketItem freeItem;\n    CartLineItem hasPriceCartLineItem;\n    CartLineItem freeCartLineItem;\n\n    public static Long itemQuantity = 3L;\n\n    public static Money itemPrice = Money.wons(3000L);\n\n    @BeforeEach\n    public void setup() {\n        given(hasPriceItem.getId()).willReturn(1L);\n        given(hasPriceItem.getPrice()).willReturn(itemPrice);\n\n        hasPriceCartLineItem =\n                CartLineItem.of(hasPriceItem, itemQuantity, List.of(cartOptionAnswer));\n\n        given(freeItem.getId()).willReturn(1L);\n        given(freeItem.getPrice()).willReturn(Money.ZERO);\n\n        freeCartLineItem =\n                CartLineItem.of(freeItem, itemQuantity, List.of(cartOptionAnswer));\n    }\n\n    @Test\n    public void 카트라인_총옵션가격_조회() {\n        // given\n        Money won2000 = Money.wons(2000L);\n        given(cartOptionAnswer.getAdditionalPrice()).willReturn(won2000);\n        // when\n        Money totalOptionsPrice = hasPriceCartLineItem.getTotalOptionsPrice();\n        // then\n        assertEquals(totalOptionsPrice, won2000);\n    }\n\n    @Test\n    public void 카트라인_총가격_조회() {\n        // given\n        Money won2000 = Money.wons(2000L);\n        given(cartOptionAnswer.getAdditionalPrice()).willReturn(won2000);\n        // when\n        Money totalCartLinePrice = hasPriceCartLineItem.getTotalCartLinePrice();\n        // then\n        assertEquals(totalCartLinePrice, won2000.plus(itemPrice).times(itemQuantity));\n    }\n\n    @Test\n    public void 카트라인_결제금액_있을때_결제필요_여부_조회() {\n        // given\n        Money won2000 = Money.wons(2000L);\n        given(cartOptionAnswer.getAdditionalPrice()).willReturn(won2000);\n        // when\n        Boolean needPaid = hasPriceCartLineItem.isNeedPaid();\n        // then\n        assertTrue(needPaid);\n    }\n\n    @Test\n    public void 카트라인_결제금액_없을때_결제필요_여부_조회() {\n        // given\n        given(cartOptionAnswer.getAdditionalPrice()).willReturn(Money.ZERO);\n        // when\n        Boolean needPaid = freeCartLineItem.isNeedPaid();\n        // then\n        assertFalse(needPaid);\n    }\n\n    @Test\n    public void 카트라인_옵션아이디목록_조회_검증() {\n        // given\n        long optionId = 1L;\n        given(cartOptionAnswer.getOptionId()).willReturn(optionId);\n        // when\n        List<Long> answerOptionIds = freeCartLineItem.getAnswerOptionIds();\n        // then\n        assertEquals(answerOptionIds, List.of(optionId));\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/cart/domain/CartOptionAnswerTest.java",
    "content": "package band.gosrock.domain.domains.cart.domain;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.BDDMockito.given;\n\nimport band.gosrock.domain.common.vo.Money;\nimport band.gosrock.domain.common.vo.OptionAnswerVo;\nimport band.gosrock.domain.domains.ticket_item.domain.Option;\nimport band.gosrock.domain.domains.ticket_item.domain.OptionGroupType;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\nclass CartOptionAnswerTest {\n    @Mock Option option;\n\n    CartOptionAnswer cartOptionAnswer;\n\n    private final Money W3000 = Money.wons(3000L);\n    private final long optionId = 1L;\n    private final String answer = \"답변\";\n\n    @BeforeEach\n    void setUp() {\n        given(option.getId()).willReturn(optionId);\n        given(option.getAdditionalPrice()).willReturn(W3000);\n\n        cartOptionAnswer = CartOptionAnswer.of(option, answer);\n    }\n\n    @Test\n    void 주문옵션답변_옵션답변정보_변환_검증() {\n        // given\n        String questionName = \"질문이름\";\n        String questionDescription = \"질문설명\";\n        OptionGroupType trueFalse = OptionGroupType.TRUE_FALSE;\n        given(option.getQuestionDescription()).willReturn(questionDescription);\n        given(option.getQuestionType()).willReturn(trueFalse);\n        given(option.getQuestionName()).willReturn(questionName);\n        OptionAnswerVo build =\n                new OptionAnswerVo(trueFalse, questionName, questionDescription, answer, W3000);\n        // when\n        OptionAnswerVo optionAnswerVo = cartOptionAnswer.getOptionAnswerVo(option);\n        assertEquals(optionAnswerVo, build);\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/cart/domain/CartTest.java",
    "content": "package band.gosrock.domain.domains.cart.domain;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.BDDMockito.willDoNothing;\n\nimport band.gosrock.domain.common.vo.Money;\nimport band.gosrock.domain.domains.cart.exception.CartLineItemNotFoundException;\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\nclass CartTest {\n\n    @Mock CartLineItem cartLineItem1;\n    @Mock CartLineItem cartLineItem2;\n    @Mock CartValidator cartValidator;\n\n    Cart hasPriceCart;\n\n    Cart freeCart;\n\n    @BeforeEach\n    void setUp() {\n        hasPriceCart =\n                Cart.forTest(1L, List.of(cartLineItem1, cartLineItem2));\n\n        freeCart = Cart.forTest(1L, List.of(cartLineItem2));\n    }\n\n    @Test\n    public void 카트_결제필요여부_로직검증() {\n        // given\n        // 유로 카트라인\n        given(cartLineItem1.isNeedPaid()).willReturn(Boolean.TRUE);\n        // 무료 카트라인\n        given(cartLineItem2.isNeedPaid()).willReturn(Boolean.FALSE);\n        // when\n        Boolean priceNeedPaid = hasPriceCart.isNeedPaid();\n        Boolean freeNeedPaid = freeCart.isNeedPaid();\n        // then\n        assertTrue(priceNeedPaid);\n        assertFalse(freeNeedPaid);\n    }\n\n    @Test\n    public void 카트_총금액_조회_로직검증() {\n        // given\n        // 유로 카트라인\n        given(cartLineItem1.getTotalCartLinePrice()).willReturn(Money.wons(3000L));\n        // 무료 카트라인\n        given(cartLineItem2.getTotalCartLinePrice()).willReturn(Money.ZERO);\n        // when\n        Money totalPrice1 = hasPriceCart.getTotalPrice();\n        Money totalPrice2 = freeCart.getTotalPrice();\n        // then\n        assertEquals(totalPrice1, Money.wons(3000L));\n        assertEquals(totalPrice2, Money.ZERO);\n    }\n\n    @Test\n    public void 카트_아이템총_수량조회_로직검증() {\n        // given\n        // 유로 카트라인\n        given(cartLineItem1.getQuantity()).willReturn(3L);\n        // 무료 카트라인\n        given(cartLineItem2.getQuantity()).willReturn(2L);\n        // when\n        Long totalQuantity = hasPriceCart.getTotalQuantity();\n        // then\n        assertEquals(totalQuantity, 5L);\n    }\n\n    @Test\n    public void 카트_아이템아이디_중복제거_조회_로직검증() {\n        // given\n        // 유로 카트라인\n        given(cartLineItem1.getItemId()).willReturn(1L);\n        // 무료 카트라인\n        given(cartLineItem2.getItemId()).willReturn(1L);\n        // when\n        List<Long> distinctItemIds = hasPriceCart.getDistinctItemIds();\n        // then\n        assertEquals(distinctItemIds, List.of(1L));\n    }\n\n    @Test\n    public void 카트_정적팩터리를_이용한생성시에_올바르게생성했는지검증() {\n        // given\n        willDoNothing().given(cartValidator).validCanCreate(any());\n        List<CartLineItem> cartLineItems = List.of(cartLineItem1);\n        Cart buildCart = Cart.forTest(1L, cartLineItems);\n        buildCart.updateCartName(\"장바구니이름\");\n        // when\n        Cart cart = Cart.of(cartLineItems, \"장바구니이름\", 1L, cartValidator);\n        // then\n        assertEquals(cart.getCartName(), buildCart.getCartName());\n    }\n\n    @Test\n    public void 카트_장바구니이름_업데이트_검증() {\n        // given\n        String cartName = \"장바구니이름\";\n        // when\n        freeCart.updateCartName(cartName);\n        // then\n        assertEquals(freeCart.getCartName(), cartName);\n    }\n\n    @Test\n    public void 카트_아이템아이디_조회_검증() {\n        // given\n        long itemId = 1L;\n        given(cartLineItem2.getItemId()).willReturn(itemId);\n        // when\n        Long findItemId = freeCart.getItemId();\n        // then\n        assertEquals(findItemId, itemId);\n    }\n\n    @Test\n    public void 카트_카트라인_한개조회시_없으면_에러발생() {\n        // given\n        Cart emptyLineCart = Cart.forTest(null, List.of());\n        // when\n        // then\n        assertThrows(CartLineItemNotFoundException.class, emptyLineCart::getCartLineItem);\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/cart/domain/CartValidatorTest.java",
    "content": "package band.gosrock.domain.domains.cart.domain;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.BDDMockito.willCallRealMethod;\nimport static org.mockito.BDDMockito.willDoNothing;\nimport static org.mockito.BDDMockito.willThrow;\n\nimport band.gosrock.domain.domains.cart.exception.CartItemNotOneTypeException;\nimport band.gosrock.domain.domains.cart.exception.CartNotAnswerAllOptionGroupException;\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor;\nimport band.gosrock.domain.domains.event.domain.Event;\nimport band.gosrock.domain.domains.event.exception.EventNotOpenException;\nimport band.gosrock.domain.domains.event.exception.EventTicketingTimeIsPassedException;\nimport band.gosrock.domain.domains.issuedTicket.adaptor.IssuedTicketAdaptor;\nimport band.gosrock.domain.domains.ticket_item.adaptor.OptionAdaptor;\nimport band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor;\nimport band.gosrock.domain.domains.ticket_item.domain.Option;\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem;\nimport band.gosrock.domain.domains.ticket_item.exception.NotCorrectOptionAnswerException;\nimport band.gosrock.domain.domains.ticket_item.exception.TicketItemQuantityLackException;\nimport band.gosrock.domain.domains.ticket_item.exception.TicketPurchaseLimitException;\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\nclass CartValidatorTest {\n\n    @Mock TicketItemAdaptor ticketItemAdaptor;\n\n    @Mock OptionAdaptor optionAdaptor;\n\n    @Mock Cart cart;\n    @Mock CartLineItem cartLineItem;\n    @Mock CartOptionAnswer cartOptionAnswer;\n    @Mock Option optionOfGroup1;\n    @Mock Option optionOfGroup2;\n    @Mock Event event;\n    @Mock TicketItem item;\n\n    @Mock IssuedTicketAdaptor issuedTicketAdaptor;\n    @Mock EventAdaptor eventAdaptor;\n\n    CartValidator cartValidator;\n\n    @BeforeEach\n    void setUp() {\n        cartValidator =\n                new CartValidator(\n                        ticketItemAdaptor, issuedTicketAdaptor, eventAdaptor, optionAdaptor);\n    }\n\n    @Test\n    public void 카트_티켓팅_가능시간검증_실패() {\n        // given\n        given(event.isTimeBeforeStartAt()).willReturn(Boolean.FALSE);\n        willCallRealMethod().given(event).validateTicketingTime();\n        // when\n        // then\n        assertThrows(\n                EventTicketingTimeIsPassedException.class,\n                () -> cartValidator.validTicketingTime(event));\n    }\n\n    @Test\n    public void 카트_티켓팅_가능시간검증_성공() {\n        // given\n        given(event.isTimeBeforeStartAt()).willReturn(Boolean.TRUE);\n        willCallRealMethod().given(event).validateTicketingTime();\n        // when\n        cartValidator.validTicketingTime(event);\n        // then\n    }\n\n    @Test\n    public void 카트_티켓팅_재고검증_실패() {\n        // given\n        willThrow(TicketItemQuantityLackException.class).given(item).validEnoughQuantity(any());\n        // when\n        // then\n        assertThrows(\n                TicketItemQuantityLackException.class,\n                () -> cartValidator.validItemStockEnough(cart, item));\n    }\n\n    @Test\n    public void 카트_티켓팅_재고검증_성공() {\n        // given\n        willDoNothing().given(item).validEnoughQuantity(any());\n        // when\n        cartValidator.validItemStockEnough(cart, item);\n        // then\n    }\n\n    @Test\n    public void 카트_티켓팅_이벤트_상태검증_성공() {\n        // given\n        willDoNothing().given(event).validateNotOpenStatus();\n        // when\n        cartValidator.validEventIsOpen(event);\n        // then\n    }\n\n    @Test\n    public void 카트_티켓팅_이벤트_상태검증_실패() {\n        // given\n        willThrow(EventNotOpenException.class).given(event).validateNotOpenStatus();\n        // when\n        // then\n        assertThrows(EventNotOpenException.class, () -> cartValidator.validEventIsOpen(event));\n    }\n\n    @Test\n    public void 카트_설문지_전부응답검증_성공() {\n        // given\n        given(cart.getCartLineItems()).willReturn(List.of(cartLineItem));\n        given(optionAdaptor.findAllByIds(any()))\n                .willReturn(List.of(optionOfGroup1, optionOfGroup2));\n        Long optionGroup1Id = 1L;\n        Long optionGroup2Id = 2L;\n\n        given(optionOfGroup1.getOptionGroupId()).willReturn(optionGroup1Id);\n        given(optionOfGroup2.getOptionGroupId()).willReturn(optionGroup2Id);\n        given(item.getOptionGroupIds()).willReturn(List.of(optionGroup1Id, optionGroup2Id));\n        // when\n        cartValidator.validAnswerToAllQuestion(cart, item);\n        // then\n    }\n\n    @Test\n    public void 카트_설문지_전부응답검증_적게대답_실패() {\n        // given\n        given(cart.getCartLineItems()).willReturn(List.of(cartLineItem));\n        given(optionAdaptor.findAllByIds(any()))\n                .willReturn(List.of(optionOfGroup1, optionOfGroup2));\n        Long optionGroup1Id = 1L;\n        Long optionGroup2Id = 2L;\n\n        given(optionOfGroup1.getOptionGroupId()).willReturn(optionGroup1Id);\n        given(optionOfGroup2.getOptionGroupId()).willReturn(optionGroup2Id);\n        Long optionGroup3Id = 3L;\n        given(item.getOptionGroupIds())\n                .willReturn(List.of(optionGroup1Id, optionGroup2Id, optionGroup3Id));\n        // when\n        // then\n        assertThrows(\n                CartNotAnswerAllOptionGroupException.class,\n                () -> cartValidator.validAnswerToAllQuestion(cart, item));\n    }\n\n    @Test\n    public void 카트_설문지_전부응답검증_많게대답_실패() {\n        // given\n        given(cart.getCartLineItems()).willReturn(List.of(cartLineItem));\n        given(optionAdaptor.findAllByIds(any()))\n                .willReturn(List.of(optionOfGroup1, optionOfGroup2));\n        Long optionGroup1Id = 1L;\n        Long optionGroup2Id = 2L;\n\n        given(optionOfGroup1.getOptionGroupId()).willReturn(optionGroup1Id);\n        given(optionOfGroup2.getOptionGroupId()).willReturn(optionGroup2Id);\n        given(item.getOptionGroupIds()).willReturn(List.of(optionGroup1Id));\n        // when\n        // then\n        assertThrows(\n                CartNotAnswerAllOptionGroupException.class,\n                () -> cartValidator.validAnswerToAllQuestion(cart, item));\n    }\n\n    @Test\n    public void 카트_설문지_전부응답검증_이상하게대답_실패() {\n        // given\n        given(cart.getCartLineItems()).willReturn(List.of(cartLineItem));\n        given(optionAdaptor.findAllByIds(any()))\n                .willReturn(List.of(optionOfGroup1, optionOfGroup2));\n        Long optionGroup1Id = 1L;\n        Long optionGroup2Id = 2L;\n\n        given(optionOfGroup1.getOptionGroupId()).willReturn(optionGroup1Id);\n        given(optionOfGroup2.getOptionGroupId()).willReturn(optionGroup2Id);\n        Long optionGroup3Id = 3L;\n        given(item.getOptionGroupIds()).willReturn(List.of(optionGroup1Id, optionGroup3Id));\n        // when\n        // then\n        assertThrows(\n                CartNotAnswerAllOptionGroupException.class,\n                () -> cartValidator.validAnswerToAllQuestion(cart, item));\n    }\n\n    @Test\n    public void 카트_설문지_올바른응답검증_이상하게대답_실패() {\n        // given\n        given(cart.getCartLineItems()).willReturn(List.of(cartLineItem));\n        given(cartLineItem.getCartOptionAnswers()).willReturn(List.of(cartOptionAnswer));\n        Long optionId = 1L;\n        given(cartOptionAnswer.getOptionId()).willReturn(optionId);\n        given(cartOptionAnswer.getAnswer()).willReturn(\"이상하게대답\");\n\n        given(optionAdaptor.findAllByIds(any())).willReturn(List.of(optionOfGroup1));\n\n        given(optionOfGroup1.getId()).willReturn(optionId);\n        willThrow(NotCorrectOptionAnswerException.class)\n                .given(optionOfGroup1)\n                .validCorrectAnswer(\"이상하게대답\");\n        // when\n        // then\n        assertThrows(\n                NotCorrectOptionAnswerException.class,\n                () -> cartValidator.validCorrectAnswer(cart));\n    }\n\n    @Test\n    public void 카트_설문지_올바른응답검증_올바르게대답_성공() {\n        // given\n        given(cart.getCartLineItems()).willReturn(List.of(cartLineItem));\n        given(cartLineItem.getCartOptionAnswers()).willReturn(List.of(cartOptionAnswer));\n        Long optionId = 1L;\n        given(cartOptionAnswer.getOptionId()).willReturn(optionId);\n        given(cartOptionAnswer.getAnswer()).willReturn(\"예\");\n\n        given(optionAdaptor.findAllByIds(any())).willReturn(List.of(optionOfGroup1));\n\n        given(optionOfGroup1.getId()).willReturn(optionId);\n        willDoNothing().given(optionOfGroup1).validCorrectAnswer(\"예\");\n\n        // when\n        cartValidator.validCorrectAnswer(cart);\n        // then\n    }\n\n    @Test\n    public void 카트_아이템_한종류가아니면_실패() {\n        // given\n        given(cart.getDistinctItemIds()).willReturn(List.of(1L, 2L));\n        // then\n        assertThrows(\n                CartItemNotOneTypeException.class,\n                () -> cartValidator.validItemKindIsOneType(cart));\n    }\n\n    @Test\n    public void 카트_아이템_구매갯수제한_실패() {\n        // given\n        given(cart.getTotalQuantity()).willReturn(3L);\n        willThrow(TicketPurchaseLimitException.EXCEPTION).given(item).validPurchaseLimit(any());\n        // then\n        assertThrows(\n                TicketPurchaseLimitException.class,\n                () -> cartValidator.validItemPurchaseLimit(cart, item));\n    }\n\n    @Test\n    public void 카트_아이템_한종류면_성공() {\n        // given\n        given(cart.getDistinctItemIds()).willReturn(List.of(2L));\n        // then\n        cartValidator.validItemKindIsOneType(cart);\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/coupon/domain/CouponCampaignTest.java",
    "content": "package band.gosrock.domain.domains.coupon.domain;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport band.gosrock.domain.common.vo.DateTimePeriod;\nimport band.gosrock.domain.domains.coupon.exception.NotIssuingCouponPeriodException;\nimport band.gosrock.domain.domains.coupon.exception.WrongDiscountAmountException;\nimport java.time.LocalDateTime;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\npublic class CouponCampaignTest {\n    CouponCampaign couponCampaign;\n\n    @BeforeEach\n    void setUp() {\n        LocalDateTime nowTime = LocalDateTime.now();\n        DateTimePeriod dateTimePeriod =\n                new DateTimePeriod(nowTime.minusDays(2), nowTime.minusDays(1));\n\n        CouponStockInfo couponStockInfo =\n                new CouponStockInfo(3L, 1L);\n        couponCampaign =\n                new CouponCampaign(null, DiscountType.PERCENTAGE, null, null, dateTimePeriod, couponStockInfo, null, null, null);\n    }\n\n    @Test\n    public void testValidatePercentageAmount() {\n        assertThrows(\n                WrongDiscountAmountException.class,\n                () -> {\n                    couponCampaign.validatePercentageAmount(DiscountType.PERCENTAGE, 101L);\n                });\n    }\n\n    @Test\n    public void testDecreaseCouponStock() {\n        // given\n        // when\n        couponCampaign.decreaseCouponStock();\n        // then\n        assertEquals(couponCampaign.getCouponStockInfo().getRemainingAmount(), 0L);\n    }\n\n    @Test\n    public void testValidateIssuePeriod() {\n        assertThrows(\n                NotIssuingCouponPeriodException.class,\n                () -> {\n                    couponCampaign.validateIssuePeriod();\n                });\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/coupon/domain/CouponStockInfoTest.java",
    "content": "package band.gosrock.domain.domains.coupon.domain;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport band.gosrock.domain.domains.coupon.exception.NoCouponStockLeftException;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\npublic class CouponStockInfoTest {\n\n    CouponStockInfo zeroCouponStockInfo;\n    CouponStockInfo leftCouponStockInfo;\n\n    @BeforeEach\n    void setUp() {\n        zeroCouponStockInfo =\n                new CouponStockInfo(3L, 0L);\n        leftCouponStockInfo =\n                new CouponStockInfo(3L, 1L);\n    }\n\n    @Test\n    public void 쿠폰_남은_재고_없음() {\n        // given\n        zeroCouponStockInfo =\n                new CouponStockInfo(3L, 0L);\n        // when, then\n        assertThrows(\n                NoCouponStockLeftException.class, () -> zeroCouponStockInfo.decreaseCouponStock());\n    }\n\n    @Test\n    public void 쿠폰_남은_재고_있음() {\n        // given\n        leftCouponStockInfo =\n                new CouponStockInfo(3L, 1L);\n        // when\n        leftCouponStockInfo.decreaseCouponStock();\n        // then\n        assertEquals(leftCouponStockInfo.getRemainingAmount(), 0L);\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/coupon/domain/IssuedCouponTest.java",
    "content": "package band.gosrock.domain.domains.coupon.domain;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.BDDMockito.given;\n\nimport band.gosrock.domain.common.vo.Money;\nimport band.gosrock.domain.domains.coupon.exception.*;\nimport java.time.LocalDateTime;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\nclass IssuedCouponTest {\n\n    static Long userId = 1L;\n    static LocalDateTime createdAt = LocalDateTime.now().minusDays(5);\n    static Long validTerm = 7L;\n    static Long discountAmount = 10000L;\n    static Long minimumCost = 50000L;\n    static Long discountPercentage = 30L;\n    IssuedCoupon issuedCoupon;\n    @Mock CouponCampaign couponCampaign;\n\n    @BeforeEach\n    void setUp() {\n        issuedCoupon = new IssuedCoupon(couponCampaign, userId);\n    }\n\n    @Test\n    void 사용가능한_쿠폰_상태_확인() {\n        // 이미 사용한 상태 쿠폰 사용\n        issuedCoupon.use();\n        // then\n        assertThrows(AlreadyUsedCouponException.class, () -> issuedCoupon.use());\n    }\n\n    @Test\n    public void use_usageStatusFalse_setUsageStatusToTrue() {\n        // 사용 안한 쿠폰 사용\n        // when\n        issuedCoupon.use();\n        // then\n        assertTrue(issuedCoupon.getUsageStatus());\n    }\n\n    @Test\n    void 내쿠폰_확인() {\n        // given\n        // when\n        issuedCoupon.validMine(userId);\n        //\n        assertThrows(NotMyCouponException.class, () -> issuedCoupon.validMine(2L));\n    }\n\n    @Test\n    public void validMine_validUserId_noExceptionThrown() {\n        // when\n        issuedCoupon.validMine(1L);\n        // then\n        assertEquals(issuedCoupon.getUserId(), 1L);\n    }\n\n    //    @Test\n    //    void testIsAvailableTerm() {\n    //        //given\n    //        given(couponCampaign.getValidTerm()).willReturn(validTerm);\n    //        //LocalDateTime localDateTime = Mockito.mock(new\n    // TypeToken<IssuedCoupon>(){}.getCreatedAt());\n    //        given(issuedCoupon.getCreatedAt()).willReturn(createdAt);\n    //        //when\n    //        Boolean result = issuedCoupon.isAvailableTerm();\n    //        //then\n    //        assertTrue(result);\n    //    }\n    //\n    //    @Test\n    //    public void testCalculateValidTerm() {\n    //        // given\n    //        given(couponCampaign.getValidTerm()).willReturn(validTerm);\n    //        given(issuedCoupon.getCreatedAt()).willReturn(createdAt);\n    //        // when\n    //        LocalDateTime result = issuedCoupon.calculateValidTerm();\n    //        // then\n    //        assertEquals(result, createdAt.plusDays(validTerm));\n    //    }\n\n    @Test\n    void testGetDiscountAmount_withAmountDiscountType() {\n        given(couponCampaign.getDiscountType()).willReturn(DiscountType.AMOUNT);\n        given(couponCampaign.getDiscountAmount()).willReturn(discountAmount);\n        given(couponCampaign.getMinimumCost()).willReturn(minimumCost);\n\n        Money supplyAmount = Money.wons(60000L);\n        Money expectedDiscountAmount = Money.wons(10000L);\n        Money actualDiscountAmount = issuedCoupon.getDiscountAmount(supplyAmount);\n\n        assertEquals(expectedDiscountAmount, actualDiscountAmount);\n    }\n\n    @Test\n    void testGetDiscountAmount_withPercentageDiscountType() {\n        given(couponCampaign.getDiscountType()).willReturn(DiscountType.PERCENTAGE);\n        given(couponCampaign.getDiscountAmount()).willReturn(discountPercentage);\n        given(couponCampaign.getMinimumCost()).willReturn(minimumCost);\n\n        Money supplyAmount = Money.wons(60000L);\n        Money expectedDiscountAmount = Money.wons(18000L);\n        Money actualDiscountAmount = issuedCoupon.getDiscountAmount(supplyAmount);\n\n        assertEquals(expectedDiscountAmount, actualDiscountAmount);\n    }\n\n    @Test\n    void testCheckSupplyAmount_withLessThanDiscount() {\n        Money supply = Money.wons(5000L);\n        Long discount = discountAmount;\n        Long minimum = minimumCost;\n\n        assertThrows(\n                SupplyLessThenDiscountException.class,\n                () -> issuedCoupon.checkSupplyAmount(supply, discount, minimum));\n    }\n\n    @Test\n    void testCheckSupplyAmount_withLessThanMinimum() {\n        Money supply = Money.wons(40000L);\n        Long discount = discountAmount;\n        Long minimum = minimumCost;\n\n        assertThrows(\n                SupplyLessThenMinimumException.class,\n                () -> issuedCoupon.checkSupplyAmount(supply, discount, minimum));\n    }\n\n    @Test\n    void testCheckSupplyAmount_withValidSupply() {\n        Money supply = Money.wons(60000L);\n        Long discount = discountAmount;\n        Long minimum = minimumCost;\n\n        Money expectedDiscountAmount = Money.wons(10000L);\n        Money actualDiscountAmount = issuedCoupon.checkSupplyAmount(supply, discount, minimum);\n\n        assertEquals(expectedDiscountAmount, actualDiscountAmount);\n    }\n\n    @Test\n    public void recovery_usageStatusFalse() {\n        // 쿠폰 사용 안한 상태\n        assertThrows(AlreadyRecoveredCouponException.class, () -> issuedCoupon.recovery());\n    }\n\n    @Test\n    public void recovery_usageStatusTrue() {\n        // 쿠폰 이미 사용한 상태\n        issuedCoupon.use();\n        issuedCoupon.recovery();\n        // then\n        assertFalse(issuedCoupon.getUsageStatus());\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/event/EventTest.java",
    "content": "package band.gosrock.domain.domains.event;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.Mockito.when;\n\nimport band.gosrock.domain.domains.event.domain.Event;\nimport band.gosrock.domain.domains.event.domain.EventBasic;\nimport band.gosrock.domain.domains.event.domain.EventStatus;\nimport band.gosrock.domain.domains.event.exception.*;\nimport java.time.LocalDateTime;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.test.util.ReflectionTestUtils;\n\n@ExtendWith(MockitoExtension.class)\npublic class EventTest {\n\n    @Mock EventBasic eventBasic;\n\n    Event event;\n\n    final LocalDateTime startAt = LocalDateTime.of(2022, 2, 15, 15, 0);\n    final Long runTime = 90L;\n\n    @BeforeEach\n    void setup() {\n        event = new Event();\n    }\n\n    @Test\n    void startAt_가져오기_테스트() {\n        // given\n        LocalDateTime expectedStartAt = startAt;\n        // when\n        when(eventBasic.getStartAt()).thenReturn(expectedStartAt);\n        event.updateEventBasic(eventBasic);\n        LocalDateTime actualStartAt = event.getStartAt();\n        // then\n        assertEquals(expectedStartAt, actualStartAt);\n    }\n\n    @Test\n    void eventBasic_null_이면_endAt도_반드시_null() {\n        // Given\n        event.updateEventBasic(null);\n        // When\n        final LocalDateTime endAt = event.getEndAt();\n        // Then\n        assertNull(endAt);\n    }\n\n    @Test\n    void endAt_가져오기_테스트() {\n        // given\n        final EventBasic eventBasic =\n                new EventBasic(null, startAt, runTime);\n        event.updateEventBasic(eventBasic);\n        // when\n        final LocalDateTime expectedEndAt = startAt.plusMinutes(runTime);\n        final LocalDateTime actualEndAt = event.getEndAt();\n        // then\n        assertEquals(expectedEndAt, actualEndAt);\n    }\n\n    //    @Test\n    //    public void eventBasic_중복수정은_불가능하다() {\n    //        // given\n    //        // reflection 으로 updated 에 true 강제 주입\n    //        ReflectionTestUtils.setField(event, \"isUpdated\", true);\n    //        // then\n    //        assertThrows(CannotModifyEventBasicException.class, () ->\n    // event.updateEventBasic(eventBasic));\n    //    }\n\n    @Test\n    public void eventBasic_업데이트_테스트() {\n        // given\n        EventBasic eventBasic =\n                new EventBasic(\"test event\", startAt, runTime);\n        // when\n        event.updateEventBasic(eventBasic);\n        // then\n        assertNotNull(event.getEventBasic());\n        assertEquals(eventBasic, event.getEventBasic());\n    }\n\n    @Test\n    void 이벤트_정산중으로_상태변경_테스트() {\n        // given: OPEN 상태에서만 CALCULATING으로 전이 가능\n        ReflectionTestUtils.setField(event, \"status\", EventStatus.OPEN);\n        final EventStatus expectedStatus = EventStatus.CALCULATING;\n        // when\n        event.calculate();\n        // then\n        assertEquals(expectedStatus, event.getStatus());\n        assertThrows(AlreadyCalculatingStatusException.class, () -> event.calculate());\n    }\n\n    @Test\n    void 이벤트_종료로_상태변경_테스트() {\n        // given: CALCULATING 상태에서만 CLOSED로 전이 가능\n        ReflectionTestUtils.setField(event, \"status\", EventStatus.CALCULATING);\n        final EventStatus expectedStatus = EventStatus.CLOSED;\n        // when\n        event.close();\n        // then\n        assertEquals(expectedStatus, event.getStatus());\n        assertThrows(AlreadyCloseStatusException.class, () -> event.close());\n    }\n\n    @Test\n    void 이벤트_오픈으로_상태변경_테스트() {\n        // given\n        final EventStatus originalStatus = event.getStatus();\n        final EventStatus expectedStatus = EventStatus.OPEN;\n        final LocalDateTime startAt = LocalDateTime.now().plusMinutes(1);\n        // when\n        when(eventBasic.getStartAt()).thenReturn(startAt);\n        event.updateEventBasic(eventBasic);\n        event.open();\n        // then\n        assertEquals(expectedStatus, event.getStatus());\n        assertNotEquals(originalStatus, expectedStatus);\n        assertThrows(AlreadyOpenStatusException.class, () -> event.open());\n    }\n\n    @Test\n    void 오픈_시간_이전인_이벤트는_오픈할수_없음() {\n        // given\n        final EventStatus originalStatus = event.getStatus();\n        final LocalDateTime startAt = LocalDateTime.now().minusMinutes(1);\n        // when\n        when(eventBasic.getStartAt()).thenReturn(startAt);\n        event.updateEventBasic(eventBasic);\n        // then\n        assertThrows(EventOpenTimeExpiredException.class, () -> event.open());\n        assertEquals(originalStatus, event.getStatus());\n    }\n\n    @Test\n    void 이벤트_준비중으로_상태변경_테스트() {\n        // given: OPEN → PREPARING 전이는 허용되지 않음\n        ReflectionTestUtils.setField(event, \"status\", EventStatus.OPEN);\n        // when & then\n        assertThrows(InvalidEventStatusTransitionException.class, () -> event.prepare());\n    }\n\n    @Test\n    void 이벤트_삭제_상태변경_테스트() {\n        // given\n        final EventStatus originalStatus = event.getStatus();\n        final EventStatus expectedStatus = EventStatus.DELETED;\n        // when\n        event.deleteSoft();\n        // then\n        assertEquals(expectedStatus, event.getStatus());\n        assertNotEquals(originalStatus, expectedStatus);\n        assertThrows(AlreadyDeletedStatusException.class, () -> event.deleteSoft());\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/host/domain/HostProfileTest.java",
    "content": "package band.gosrock.domain.domains.host.domain;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\npublic class HostProfileTest {\n\n    @Mock Host host;\n\n    HostProfile hostProfile;\n\n    @BeforeEach\n    void setup() {\n        hostProfile = new HostProfile();\n    }\n\n    @Test\n    void 호스트_프로필_업데이트_테스트() {\n        // given\n        final HostProfile newHostProfile =\n                new HostProfile(\"테스트\", \"123\", \"key\", \"22@cc.com\", \"010-0000-0000\");\n        // when\n        hostProfile.updateProfile(newHostProfile);\n        // then\n        assertEquals(hostProfile.getProfileImage(), newHostProfile.getProfileImage());\n        assertEquals(hostProfile.getContactEmail(), newHostProfile.getContactEmail());\n        assertEquals(hostProfile.getContactNumber(), newHostProfile.getContactNumber());\n        assertEquals(hostProfile.getIntroduce(), newHostProfile.getIntroduce());\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/host/domain/HostRoleTest.java",
    "content": "package band.gosrock.domain.domains.host.domain;\n\nimport static band.gosrock.domain.domains.host.domain.HostRole.*;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\npublic class HostRoleTest {\n\n    @Test\n    void ENUM_값에_해당하지_않으면_NULL_반환해야한다() {\n        // given\n        // when\n        // then\n        assertEquals(HostRole.fromHostRole(\"MASTER\"), MASTER);\n        assertEquals(HostRole.fromHostRole(\"MANAGER\"), MANAGER);\n        assertEquals(HostRole.fromHostRole(\"GUEST\"), GUEST);\n        assertNull(HostRole.fromHostRole(\"NOT_PROVIDED\"));\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/host/domain/HostTest.java",
    "content": "package band.gosrock.domain.domains.host.domain;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.Mockito.verify;\n\nimport band.gosrock.domain.common.vo.HostInfoVo;\nimport band.gosrock.domain.common.vo.HostProfileVo;\nimport band.gosrock.domain.domains.host.exception.*;\nimport java.util.Set;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.springframework.test.util.ReflectionTestUtils;\n\n@ExtendWith(MockitoExtension.class)\npublic class HostTest {\n    @Mock HostUser masterHostUser;\n    @Mock HostUser managerHostUser;\n    @Mock HostUser guestHostUser;\n    @Mock HostProfile hostProfile;\n    Host host;\n    final Long masterUserId = 1L;\n    final Long managerUserId = 2L;\n    final Long guestUserId = 3L;\n\n    @BeforeEach\n    void setup() {\n        host = new Host(1L, null, null, null, null, null, null);\n    }\n\n    @Test\n    public void 호스트에_유저_1명_추가기능_검증() {\n        // given\n        given(managerHostUser.getUserId()).willReturn(managerUserId);\n        // when\n        host.addHostUsers(Set.of(managerHostUser));\n        // then\n        final HostUser findUser =\n                host.getHostUsers().stream()\n                        .filter(hostUser -> hostUser.getUserId().equals(managerUserId))\n                        .findFirst()\n                        .orElse(null);\n        assertEquals(findUser, managerHostUser);\n    }\n\n    @Test\n    public void 호스트유저_추가후_유저ID로_가져오기_검증() {\n        // given\n        given(masterHostUser.getUserId()).willReturn(masterUserId);\n        // when\n        host.addHostUsers(Set.of(masterHostUser));\n        // then\n        final HostUser findUser =\n                host.getHostUsers().stream()\n                        .filter(hostUser -> hostUser.getUserId().equals(masterUserId))\n                        .findFirst()\n                        .orElse(null);\n        assertEquals(host.getHostUserByUserId(masterUserId), findUser);\n    }\n\n    @Test\n    public void 호스트유저ID로_포함여부_검증() {\n        // given\n        given(masterHostUser.getUserId()).willReturn(masterUserId);\n        // when\n        host.addHostUsers(Set.of(masterHostUser));\n        // then\n        assertTrue(host.hasHostUserId(masterUserId));\n    }\n\n    @Test\n    public void 호스트유저로_포함여부_검증() {\n        // given\n        given(masterHostUser.getUserId()).willReturn(masterUserId);\n        // when\n        host.addHostUsers(Set.of(masterHostUser));\n        // then\n        assertTrue(host.hasHostUser(masterHostUser));\n    }\n\n    @Test\n    public void 호스트에_유저_여러명_추가기능_검증() {\n        // given\n        given(masterHostUser.getUserId()).willReturn(masterUserId);\n        given(managerHostUser.getUserId()).willReturn(managerUserId);\n        // when\n        host.addHostUsers(Set.of(masterHostUser, managerHostUser));\n        // then\n        assertEquals(host.getHostUserByUserId(masterUserId), masterHostUser);\n        assertEquals(host.getHostUserByUserId(managerUserId), managerHostUser);\n    }\n\n    @Test\n    public void 호스트에_유저_초대기능_검증() {\n        // given\n        given(managerHostUser.getUserId()).willReturn(managerUserId);\n        // when\n        host.inviteHostUsers(Set.of(managerHostUser));\n        // then\n        assertEquals(host.getHostUserByUserId(managerUserId), managerHostUser);\n    }\n\n    @Test\n    public void 이미있는_호스트유저는_초대불가능() {\n        // given\n        given(managerHostUser.getUserId()).willReturn(managerUserId);\n        // when\n        host.addHostUsers(Set.of(managerHostUser));\n        // then\n        assertThrows(\n                AlreadyJoinedHostException.class,\n                () -> host.inviteHostUsers(Set.of(managerHostUser)));\n    }\n\n    @Test\n    public void 호스트_프로필_변경_호출되었는지_검증() {\n        // given\n        given(hostProfile.getIntroduce()).willReturn(\"소개\");\n        given(hostProfile.getContactNumber()).willReturn(\"num\");\n        given(hostProfile.getProfileImage()).willReturn(null);\n        given(hostProfile.getContactEmail()).willReturn(\"mail\");\n        // when\n        host.updateProfile(hostProfile);\n        // then\n        assertEquals(host.getProfile().getIntroduce(), hostProfile.getIntroduce());\n        assertEquals(host.getProfile().getContactEmail(), hostProfile.getContactEmail());\n        assertEquals(host.getProfile().getContactNumber(), hostProfile.getContactNumber());\n        assertEquals(host.getProfile().getProfileImage(), hostProfile.getProfileImage());\n    }\n\n    @Test\n    public void 슬랙알림용_URL변경_검증() {\n        // given\n        final String url = \"https://mmm.test\";\n        // when\n        host.updateSlackUrl(url);\n        // then\n        assertEquals(host.getSlackUrl(), url);\n    }\n\n    @Test\n    public void 슬랙알림용_URL은_이미있는_값과_같으면_안된다() {\n        // given\n        final String url = \"https://mmm.test\";\n        host.updateSlackUrl(url);\n        // when\n        // then\n        assertThrows(DuplicateSlackUrlException.class, () -> host.updateSlackUrl(url));\n    }\n\n    @Test\n    public void 마스터호스트_역할변경_불가_검증() {\n        // given\n        // then\n        // @BeforeEach 에서 마스터 ID 는 현재 1임\n        assertThrows(\n                CannotModifyMasterHostRoleException.class,\n                () -> host.setHostUserRole(masterUserId, HostRole.MANAGER));\n    }\n\n    @Test\n    public void 호스트_역할변경_검증() {\n        // given\n        given(masterHostUser.getUserId()).willReturn(masterUserId);\n        given(managerHostUser.getUserId()).willReturn(managerUserId);\n        // when\n        host.addHostUsers(Set.of(masterHostUser, managerHostUser));\n        // then\n        host.setHostUserRole(managerUserId, HostRole.MANAGER);\n        verify(managerHostUser).setHostRole(HostRole.MANAGER);\n        // HostUser 에서 역할 변경 호출시 성공\n    }\n\n    @Test\n    public void 호스트유저_매니저인지_검증() {\n        // given\n        given(masterHostUser.getUserId()).willReturn(masterUserId);\n        given(masterHostUser.getRole()).willReturn(HostRole.MASTER);\n        given(managerHostUser.getUserId()).willReturn(managerUserId);\n        given(managerHostUser.getRole()).willReturn(HostRole.MANAGER);\n        given(guestHostUser.getUserId()).willReturn(guestUserId);\n        given(guestHostUser.getRole()).willReturn(HostRole.GUEST);\n        // when\n        host.addHostUsers(Set.of(masterHostUser, managerHostUser, guestHostUser));\n        // then\n        // 게스트, 마스터는 매니저 x\n        assertFalse(host.isManagerHostUserId(masterUserId));\n        assertTrue(host.isManagerHostUserId(managerUserId));\n        assertFalse(host.isManagerHostUserId(guestUserId));\n    }\n\n    @Test\n    public void 초대_수락한_호스트유저인지_검증() {\n        // given\n        given(managerHostUser.getUserId()).willReturn(managerUserId);\n        given(managerHostUser.getActive()).willReturn(false);\n        given(guestHostUser.getUserId()).willReturn(guestUserId);\n        given(guestHostUser.getActive()).willReturn(true);\n        // when\n        host.addHostUsers(Set.of(managerHostUser, guestHostUser));\n        // then\n        assertFalse(host.isActiveHostUserId(managerUserId));\n        assertTrue(host.isActiveHostUserId(guestUserId));\n    }\n\n    @Test\n    public void 이미_가입한_호스트_ID로_검증_테스트() {\n        given(managerHostUser.getUserId()).willReturn(managerUserId);\n        // when\n        host.addHostUsers(Set.of(managerHostUser));\n        // then\n        assertThrows(\n                AlreadyJoinedHostException.class,\n                () -> host.validateHostUserIdExistence(managerUserId));\n        assertDoesNotThrow(() -> host.validateHostUserIdExistence(guestUserId));\n    }\n\n    @Test\n    public void 이미_가입한_호스트_호스트유저로_검증_테스트() {\n        given(managerHostUser.getUserId()).willReturn(managerUserId);\n        given(guestHostUser.getUserId()).willReturn(guestUserId);\n        // when\n        host.addHostUsers(Set.of(managerHostUser));\n        // then\n        assertThrows(\n                AlreadyJoinedHostException.class,\n                () -> host.validateHostUserExistence(managerHostUser));\n        assertDoesNotThrow(() -> host.validateHostUserExistence(guestHostUser));\n    }\n\n    @Test\n    public void 호스트유저_종속_검증_테스트() {\n        given(managerHostUser.getUserId()).willReturn(managerUserId);\n        // when\n        host.addHostUsers(Set.of(managerHostUser));\n        // then\n        assertDoesNotThrow(() -> host.validateHostUser(managerUserId));\n        assertThrows(ForbiddenHostException.class, () -> host.validateHostUser(guestUserId));\n    }\n\n    @Test\n    public void 호스트유저_게스트권한_검증_테스트() {\n        // given\n        given(managerHostUser.getUserId()).willReturn(managerUserId);\n        given(managerHostUser.getActive()).willReturn(false);\n        given(guestHostUser.getUserId()).willReturn(guestUserId);\n        given(guestHostUser.getActive()).willReturn(true);\n        // when\n        host.addHostUsers(Set.of(managerHostUser, guestHostUser));\n        // then\n        assertThrows(\n                NotAcceptedHostException.class, () -> host.validateActiveHostUser(managerUserId));\n        assertDoesNotThrow(() -> host.validateActiveHostUser(guestUserId));\n    }\n\n    @Test\n    public void 호스트유저_매니저권한_검증_테스트() {\n        // given\n        given(masterHostUser.getUserId()).willReturn(masterUserId);\n        given(masterHostUser.getActive()).willReturn(true);\n        given(masterHostUser.getRole()).willReturn(HostRole.MASTER);\n        given(managerHostUser.getUserId()).willReturn(managerUserId);\n        given(managerHostUser.getActive()).willReturn(true);\n        given(managerHostUser.getRole()).willReturn(HostRole.MANAGER);\n        given(guestHostUser.getUserId()).willReturn(guestUserId);\n        given(guestHostUser.getActive()).willReturn(true);\n        given(guestHostUser.getRole()).willReturn(HostRole.GUEST);\n        // when\n        host.addHostUsers(Set.of(masterHostUser, managerHostUser, guestHostUser));\n        // then\n        assertThrows(\n                NotManagerHostException.class, () -> host.validateManagerHostUser(guestUserId));\n        assertDoesNotThrow(() -> host.validateManagerHostUser(managerUserId));\n        assertDoesNotThrow(() -> host.validateManagerHostUser(masterUserId));\n    }\n\n    @Test\n    public void 호스트유저_마스터_검증_테스트() {\n        // given\n        given(masterHostUser.getUserId()).willReturn(masterUserId);\n        given(masterHostUser.getActive()).willReturn(true);\n        given(managerHostUser.getUserId()).willReturn(managerUserId);\n        given(managerHostUser.getActive()).willReturn(true);\n        // when\n        host.addHostUsers(Set.of(masterHostUser, managerHostUser));\n        // then\n        assertThrows(\n                NotMasterHostException.class, () -> host.validateMasterHostUser(managerUserId));\n        assertDoesNotThrow(() -> host.validateMasterHostUser(masterUserId));\n    }\n\n    @Test\n    public void 호스트_파트너여부_검증_테스트() {\n        // given\n        // reflection 으로 partner 에 true 강제 주입\n        final Host partnerHost = new Host();\n        ReflectionTestUtils.setField(partnerHost, \"partner\", true);\n        // when\n        // then\n        assertThrows(NotPartnerHostException.class, () -> host.validatePartnerHost());\n        assertDoesNotThrow(partnerHost::validatePartnerHost);\n    }\n\n    @Test\n    public void 호스트_vo_변환_테스트() {\n        // given\n        // when\n        // then\n        assertEquals(host.toHostInfoVo(), HostInfoVo.from(host));\n        assertEquals(host.toHostProfileVo(), HostProfileVo.from(host));\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/host/domain/HostUserTest.java",
    "content": "package band.gosrock.domain.domains.host.domain;\n\nimport static band.gosrock.domain.domains.host.domain.HostRole.MANAGER;\nimport static band.gosrock.domain.domains.host.domain.HostRole.MASTER;\nimport static java.lang.Boolean.TRUE;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\n\nimport band.gosrock.domain.domains.host.exception.AlreadyJoinedHostException;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\npublic class HostUserTest {\n\n    @Mock Host host;\n    HostUser hostUser;\n\n    @BeforeEach\n    void setup() {\n        hostUser = new HostUser(host, null, MASTER);\n    }\n\n    @Test\n    void 호스트유저_권한변경_테스트() {\n        // given\n        final HostRole role = MANAGER;\n        // when\n        hostUser.setHostRole(role);\n        // then\n        assertEquals(hostUser.getRole(), role);\n    }\n\n    @Test\n    void 호스트유저_초대수락으로_활성화_테스트() {\n        // given\n        // when\n        hostUser.activate();\n        // then\n        assertEquals(hostUser.getActive(), TRUE);\n    }\n\n    @Test\n    void 호스트유저_초대_중복수락은_불가능하다() {\n        // given\n        // when\n        hostUser.activate();\n        // then\n        assertThrows(AlreadyJoinedHostException.class, () -> hostUser.activate());\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/host/service/HostServiceConcurrencyFailureTest.java",
    "content": "package band.gosrock.domain.domains.host.service;\n\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.BDDMockito.given;\n\nimport band.gosrock.domain.CunCurrencyExecutorService;\nimport band.gosrock.domain.DisableDomainEvent;\nimport band.gosrock.domain.DisableRedissonLock;\nimport band.gosrock.domain.DomainIntegrateSpringBootTest;\nimport band.gosrock.domain.domains.host.domain.Host;\nimport band.gosrock.domain.domains.host.domain.HostUser;\nimport band.gosrock.domain.domains.host.repository.HostRepository;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.Mock;\nimport org.redisson.api.RedissonClient;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.mock.mockito.MockBean;\nimport org.springframework.test.util.ReflectionTestUtils;\n\n@DomainIntegrateSpringBootTest\n@DisableDomainEvent\n@DisableRedissonLock\n@Slf4j\npublic class HostServiceConcurrencyFailureTest {\n    @Autowired HostService hostService;\n\n    @Autowired RedissonClient redissonClient;\n\n    Host host;\n\n    @Mock HostUser hostUser;\n    @MockBean HostRepository hostRepository;\n\n    @BeforeEach\n    void setup() {\n        host = new Host();\n        ReflectionTestUtils.setField(host, \"id\", 1L);\n        given(hostUser.getUserId()).willReturn(9L);\n        given(hostRepository.save(any(Host.class))).willReturn(host);\n    }\n\n    @Test\n    @DisplayName(\"락 적용을 안하면 여러개의 초대가 승인될 수도 있다.\")\n    public void 호스트유저_초대요청_동시성_실패() throws InterruptedException {\n        // given\n        // when\n        AtomicLong successCount = new AtomicLong();\n        CunCurrencyExecutorService.execute(\n                () -> hostService.inviteHostUser(host, hostUser), successCount);\n        // then\n        log.info(String.valueOf(successCount.get()));\n        assertThat(successCount.get()).isGreaterThanOrEqualTo(1L);\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/host/service/HostServiceConcurrencyTest.java",
    "content": "package band.gosrock.domain.domains.host.service;\n\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.BDDMockito.given;\n\nimport band.gosrock.domain.CunCurrencyExecutorService;\nimport band.gosrock.domain.DisableDomainEvent;\nimport band.gosrock.domain.DomainIntegrateSpringBootTest;\nimport band.gosrock.domain.domains.host.domain.Host;\nimport band.gosrock.domain.domains.host.domain.HostUser;\nimport band.gosrock.domain.domains.host.repository.HostRepository;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.Mock;\nimport org.redisson.api.RedissonClient;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.mock.mockito.MockBean;\nimport org.springframework.test.util.ReflectionTestUtils;\n\n@DomainIntegrateSpringBootTest\n@DisableDomainEvent\n@Slf4j\npublic class HostServiceConcurrencyTest {\n    @Autowired HostService hostService;\n\n    @Autowired RedissonClient redissonClient;\n\n    Host host;\n\n    @Mock HostUser hostUser;\n    @MockBean HostRepository hostRepository;\n\n    @BeforeEach\n    void setup() {\n        host = new Host();\n        ReflectionTestUtils.setField(host, \"id\", 1L);\n        given(hostUser.getUserId()).willReturn(9L);\n        given(hostRepository.save(any(Host.class))).willReturn(host);\n    }\n\n    @Test\n    @DisplayName(\"동시에 초대 요청을 보내도 하나의 요청만 성공해야한다.\")\n    public void 호스트유저_초대요청_동시성테스트() throws InterruptedException {\n        // given\n        // when\n        AtomicLong successCount = new AtomicLong();\n        CunCurrencyExecutorService.execute(\n                () -> hostService.inviteHostUser(host, hostUser), successCount);\n        // then\n        assertThat(successCount.get()).isEqualTo(1L);\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/issuedTicket/adaptor/IssuedTicketAdaptorTest.java",
    "content": "package band.gosrock.domain.domains.issuedTicket.adaptor;\n\n\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\npublic class IssuedTicketAdaptorTest {}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/issuedTicket/domain/IssuedTicketItemInfoVoTest.java",
    "content": "package band.gosrock.domain.domains.issuedTicket.domain;\n\nimport static org.junit.jupiter.api.Assertions.assertAll;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport band.gosrock.domain.common.vo.Money;\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem;\nimport band.gosrock.domain.domains.ticket_item.domain.TicketPayType;\nimport band.gosrock.domain.domains.ticket_item.domain.TicketType;\nimport java.time.LocalDateTime;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\npublic class IssuedTicketItemInfoVoTest {\n\n    IssuedTicketItemInfoVo itemInfoVo;\n\n    private final TicketType ticketType = TicketType.FIRST_COME_FIRST_SERVED;\n\n    private final TicketPayType payType = TicketPayType.DUDOONG_TICKET;\n\n    private final Money w3000 = Money.wons(3000L);\n\n    private final LocalDateTime startAt = LocalDateTime.now();\n\n    private final LocalDateTime endAt = startAt.plusDays(1L);\n\n    @BeforeEach\n    void setUp() {\n        itemInfoVo =\n                new IssuedTicketItemInfoVo(1L, ticketType, payType, \"testTicket\", w3000);\n    }\n\n    @Test\n    public void 티켓_아이템_정보를_발급티켓_아이템_인포로_정상적으로_변환_테스트() {\n        // given\n        TicketItem newTicketItem =\n                new TicketItem(TicketPayType.DUDOONG_TICKET, \"testTicket\", \"test\", w3000, 1L, 1L, 1L, ticketType, \"test\", \"test\", \"test\", true, true, startAt, endAt, 1L);\n\n        // when\n        IssuedTicketItemInfoVo itemInfoVoForTest = IssuedTicketItemInfoVo.from(newTicketItem);\n\n        // then\n        assertAll(\n                () -> assertEquals(itemInfoVo.getPrice(), itemInfoVoForTest.getPrice()),\n                () -> assertEquals(itemInfoVo.getTicketName(), itemInfoVoForTest.getTicketName()),\n                () -> assertEquals(itemInfoVo.getTicketType(), itemInfoVoForTest.getTicketType()),\n                () -> assertEquals(itemInfoVo.getPayType(), itemInfoVoForTest.getPayType()));\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/issuedTicket/domain/IssuedTicketOptionAnswerTest.java",
    "content": "package band.gosrock.domain.domains.issuedTicket.domain;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.BDDMockito.given;\n\nimport band.gosrock.domain.common.vo.Money;\nimport band.gosrock.domain.common.vo.OptionAnswerVo;\nimport band.gosrock.domain.domains.ticket_item.domain.Option;\nimport band.gosrock.domain.domains.ticket_item.domain.OptionGroupType;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\npublic class IssuedTicketOptionAnswerTest {\n\n    @Mock Option option;\n\n    IssuedTicketOptionAnswer issuedTicketOptionAnswer;\n\n    private final Money W3000 = Money.wons(3000L);\n    private final Long optionId = 1L;\n    private final String answer = \"답변\";\n\n    private final Long orderOptionAnswerId = 1L;\n\n    @BeforeEach\n    void setUp() {\n        // option 세팅\n        given(option.getAdditionalPrice()).willReturn(W3000);\n\n        issuedTicketOptionAnswer =\n                new IssuedTicketOptionAnswer(optionId, Money.ZERO, answer);\n    }\n\n    @Test\n    void 주문옵션답변_발급티켓옵션답변_변환_테스트() {\n        // given\n        String questionName = \"questionName\";\n        String questionDescription = \"questionDescription\";\n        OptionGroupType optionGroupType = OptionGroupType.TRUE_FALSE;\n\n        given(option.getQuestionName()).willReturn(questionName);\n        given(option.getQuestionDescription()).willReturn(questionDescription);\n        given(option.getQuestionType()).willReturn(optionGroupType);\n\n        OptionAnswerVo optionAnswerVoForTest =\n                new OptionAnswerVo(optionGroupType, questionName, questionDescription, answer, W3000);\n\n        OptionAnswerVo optionAnswerVo = issuedTicketOptionAnswer.getOptionAnswerVo(option);\n\n        assertEquals(optionAnswerVo, optionAnswerVoForTest);\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/issuedTicket/domain/IssuedTicketTest.java",
    "content": "package band.gosrock.domain.domains.issuedTicket.domain;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.mockito.BDDMockito.given;\n\nimport band.gosrock.domain.common.vo.Money;\nimport band.gosrock.domain.domains.issuedTicket.exception.CanNotCancelEntranceException;\nimport band.gosrock.domain.domains.issuedTicket.exception.CanNotCancelException;\nimport band.gosrock.domain.domains.issuedTicket.exception.CanNotEntranceException;\nimport band.gosrock.domain.domains.issuedTicket.exception.IssuedTicketAlreadyEntranceException;\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\npublic class IssuedTicketTest {\n\n    @Mock IssuedTicketOptionAnswer issuedTicketOptionAnswer;\n\n    @Mock IssuedTicketOptionAnswer issuedTicketOptionAnswer1;\n\n    @Mock IssuedTicketUserInfoVo userInfoVo;\n\n    @Mock IssuedTicketItemInfoVo itemInfoVo;\n\n    IssuedTicket issuedTicket;\n\n    IssuedTicket canceledIssuedTicket;\n\n    IssuedTicket entranceIssuedTicket;\n\n    private final String orderUuid = \"testing\";\n\n    private final Money W3000 = Money.wons(3000L);\n\n    @BeforeEach\n    void setUp() {\n        issuedTicket =\n                new IssuedTicket(1L, userInfoVo, orderUuid, 1L, itemInfoVo, IssuedTicketStatus.ENTRANCE_INCOMPLETE, List.of(issuedTicketOptionAnswer, issuedTicketOptionAnswer1));\n\n        canceledIssuedTicket =\n                new IssuedTicket(1L, userInfoVo, orderUuid, 1L, itemInfoVo, IssuedTicketStatus.CANCELED, List.of(issuedTicketOptionAnswer, issuedTicketOptionAnswer1));\n\n        entranceIssuedTicket =\n                new IssuedTicket(1L, userInfoVo, orderUuid, 1L, itemInfoVo, IssuedTicketStatus.ENTRANCE_COMPLETED, List.of(issuedTicketOptionAnswer, issuedTicketOptionAnswer1));\n    }\n\n    @Test\n    public void 발급티켓_옵션들_합_검증() {\n        // given\n        Money w1500 = Money.wons(1500L);\n        Money w4500 = Money.wons(4500L);\n\n        given(issuedTicketOptionAnswer.getAdditionalPrice()).willReturn(w1500);\n        given(issuedTicketOptionAnswer1.getAdditionalPrice()).willReturn(w4500);\n\n        // when\n        Money sumOptionPrice = issuedTicket.sumOptionPrice();\n\n        // then\n        Money w6000 = Money.wons(6000L);\n        assertEquals(w6000, sumOptionPrice);\n    }\n\n    @Test\n    public void 발급티켓_취소_로직_검증() {\n        // given\n        // when\n        issuedTicket.cancel();\n        // then\n        assertEquals(IssuedTicketStatus.CANCELED, issuedTicket.getIssuedTicketStatus());\n    }\n\n    @Test\n    public void 이미취소된_발급티켓_취소시_에러_검증() {\n        // given\n        // when\n        // then\n        assertThrows(CanNotCancelException.class, () -> canceledIssuedTicket.cancel());\n    }\n\n    @Test\n    public void 발급티켓_입장_로직_검증() {\n        // given\n        // when\n        issuedTicket.entrance();\n        // then\n        assertEquals(IssuedTicketStatus.ENTRANCE_COMPLETED, issuedTicket.getIssuedTicketStatus());\n    }\n\n    @Test\n    public void 취소티켓_입장요청시_에러_검증() {\n        // given\n        // when\n        // then\n        assertThrows(CanNotEntranceException.class, () -> canceledIssuedTicket.entrance());\n    }\n\n    @Test\n    public void 이미입장된티켓_입장요청시_에러_검증() {\n        // given\n        // when\n        // then\n        assertThrows(\n                IssuedTicketAlreadyEntranceException.class, () -> entranceIssuedTicket.entrance());\n    }\n\n    @Test\n    public void 입장티켓_입장_취소_로직_검증() {\n        // given\n        // when\n        entranceIssuedTicket.entranceCancel();\n        // then\n        assertEquals(\n                IssuedTicketStatus.ENTRANCE_INCOMPLETE,\n                entranceIssuedTicket.getIssuedTicketStatus());\n    }\n\n    @Test\n    public void 입장취소요청시_에러_검증() {\n        // given\n        // when\n        // then\n        assertThrows(\n                CanNotCancelEntranceException.class, () -> canceledIssuedTicket.entranceCancel());\n        assertThrows(CanNotCancelEntranceException.class, () -> issuedTicket.entranceCancel());\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/issuedTicket/domain/IssuedTicketUserInfoVoTest.java",
    "content": "package band.gosrock.domain.domains.issuedTicket.domain;\n\nimport static org.junit.jupiter.api.Assertions.assertAll;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.BDDMockito.given;\n\nimport band.gosrock.domain.common.vo.PhoneNumberVo;\nimport band.gosrock.domain.domains.user.domain.Profile;\nimport band.gosrock.domain.domains.user.domain.User;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\npublic class IssuedTicketUserInfoVoTest {\n\n    @Mock User user;\n\n    IssuedTicketUserInfoVo userInfoVo;\n\n    Profile profile;\n\n    private final PhoneNumberVo phoneNumberVo = PhoneNumberVo.valueOf(\"010-1234-5678\");\n\n    @BeforeEach\n    void setUp() {\n        userInfoVo =\n                new IssuedTicketUserInfoVo(1L, \"test\", \"test@test.com\", phoneNumberVo);\n        profile =\n                new Profile(\"test\", \"test@test.com\", \"010-1234-5678\", \"test\");\n    }\n\n    @Test\n    public void 유저_정보를_발급티켓_유저_인포로_변환_테스트() {\n        // given\n        given(user.getProfile()).willReturn(profile);\n\n        // when\n        IssuedTicketUserInfoVo userInfoVoForTest = IssuedTicketUserInfoVo.from(user);\n\n        // then\n        assertAll(\n                () -> assertEquals(userInfoVo.getUserName(), userInfoVoForTest.getUserName()),\n                () ->\n                        assertEquals(\n                                userInfoVo.getPhoneNumber().getPhoneNumber(),\n                                userInfoVoForTest.getPhoneNumber().getPhoneNumber()),\n                () -> assertEquals(userInfoVo.getEmail(), userInfoVoForTest.getEmail()));\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/issuedTicket/domain/validator/IssuedTicketValidatorTest.java",
    "content": "package band.gosrock.domain.domains.issuedTicket.domain.validator;\n\nimport static org.mockito.BDDMockito.given;\n\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket;\nimport band.gosrock.domain.domains.issuedTicket.exception.IssuedTicketNotMatchedEventException;\nimport band.gosrock.domain.domains.issuedTicket.validator.IssuedTicketValidator;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\npublic class IssuedTicketValidatorTest {\n\n    @Mock IssuedTicket issuedTicket;\n\n    IssuedTicketValidator issuedTicketValidator;\n\n    @BeforeEach\n    public void setUp() {\n        issuedTicketValidator = new IssuedTicketValidator();\n    }\n\n    @Test\n    public void 발급티켓_이벤트_검증_성공() {\n        // given\n        given(issuedTicket.getEventId()).willReturn(1L);\n        // when\n        issuedTicketValidator.validIssuedTicketEventIdEqualEvent(issuedTicket, 1L);\n    }\n\n    @Test\n    public void 발급티켓_이벤트_검증_실패() {\n        // given\n        given(issuedTicket.getEventId()).willReturn(2L);\n        // when\n        // then\n        Assertions.assertThrows(\n                IssuedTicketNotMatchedEventException.class,\n                () -> issuedTicketValidator.validIssuedTicketEventIdEqualEvent(issuedTicket, 1L));\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/issuedTicket/service/IssuedTicketDomainServiceTest.java",
    "content": "package band.gosrock.domain.domains.issuedTicket.service;\n\nimport static org.junit.jupiter.api.Assertions.assertAll;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.BDDMockito.*;\n\nimport band.gosrock.domain.DisableDomainEvent;\nimport band.gosrock.domain.DomainIntegrateSpringBootTest;\nimport band.gosrock.domain.common.vo.Money;\nimport band.gosrock.domain.domains.issuedTicket.adaptor.IssuedTicketAdaptor;\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket;\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketItemInfoVo;\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketOptionAnswer;\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketStatus;\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketUserInfoVo;\nimport band.gosrock.domain.domains.issuedTicket.validator.IssuedTicketValidator;\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor;\nimport band.gosrock.domain.domains.order.domain.Order;\nimport band.gosrock.domain.domains.order.domain.OrderLineItem;\nimport band.gosrock.domain.domains.order.domain.OrderOptionAnswer;\nimport band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor;\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem;\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor;\nimport band.gosrock.domain.domains.user.domain.User;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.Mock;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.mock.mockito.MockBean;\n\n@DomainIntegrateSpringBootTest\n@DisableDomainEvent\npublic class IssuedTicketDomainServiceTest {\n\n    @Autowired IssuedTicketDomainService issuedTicketDomainService;\n\n    @MockBean UserAdaptor userAdaptor;\n\n    @MockBean TicketItemAdaptor ticketItemAdaptor;\n\n    @MockBean OrderAdaptor orderAdaptor;\n\n    @MockBean IssuedTicketAdaptor issuedTicketAdaptor;\n\n    @Mock IssuedTicket issuedTicket;\n\n    @Mock IssuedTicket issuedTicket1;\n\n    @Mock IssuedTicket issuedTicket2;\n\n    @Mock IssuedTicketOptionAnswer issuedTicketOptionAnswer;\n\n    @Mock IssuedTicketOptionAnswer issuedTicketOptionAnswer1;\n\n    @Mock TicketItem ticketItem;\n\n    @Mock OrderLineItem orderLineItem;\n\n    @Mock OrderLineItem orderLineItem1;\n\n    @Mock OrderOptionAnswer orderOptionAnswer;\n\n    @Mock User user;\n\n    @Mock Order order;\n\n    @Mock IssuedTicketItemInfoVo itemInfoVo;\n\n    @Mock IssuedTicketItemInfoVo itemInfoVo1;\n\n    @Mock IssuedTicketUserInfoVo userInfoVo;\n\n    @Mock IssuedTicketValidator issuedTicketValidator;\n\n    private final String orderUuid = \"orderUuid\";\n\n    private final List<IssuedTicket> issuedTickets = new ArrayList<>();\n\n    private final List<OrderLineItem> orderLineItems = new ArrayList<>();\n\n    private final Money w3000 = Money.wons(3000L);\n\n    @BeforeEach\n    void setUp() {\n        // Todo: orderLineItem 생성\n\n        issuedTicket =\n                new IssuedTicket(1L, userInfoVo, orderUuid, 1L, itemInfoVo, IssuedTicketStatus.ENTRANCE_INCOMPLETE, List.of(issuedTicketOptionAnswer, issuedTicketOptionAnswer1));\n\n        issuedTicket.createUUID();\n        issuedTickets.add(issuedTicket);\n\n        issuedTicket1 =\n                new IssuedTicket(1L, userInfoVo, orderUuid, 1L, itemInfoVo, IssuedTicketStatus.ENTRANCE_INCOMPLETE, List.of(issuedTicketOptionAnswer, issuedTicketOptionAnswer1));\n\n        issuedTicket1.createUUID();\n        issuedTickets.add(issuedTicket1);\n\n        issuedTicket2 =\n                new IssuedTicket(1L, userInfoVo, orderUuid, 1L, itemInfoVo1, IssuedTicketStatus.ENTRANCE_INCOMPLETE, List.of(issuedTicketOptionAnswer, issuedTicketOptionAnswer1));\n\n        issuedTicket2.createUUID();\n\n        IssuedTicketDomainService issuedTicketDomainService =\n                new IssuedTicketDomainService(\n                        issuedTicketAdaptor,\n                        ticketItemAdaptor,\n                        userAdaptor,\n                        orderAdaptor,\n                        issuedTicketValidator);\n\n        orderLineItems.add(orderLineItem);\n        orderLineItems.add(orderLineItem1);\n    }\n\n    @Test\n    public void 티켓_취소_로직_정상_작동_테스트() {\n        IssuedTicketDomainService issuedTicketDomainService =\n                new IssuedTicketDomainService(\n                        issuedTicketAdaptor,\n                        ticketItemAdaptor,\n                        userAdaptor,\n                        orderAdaptor,\n                        issuedTicketValidator);\n\n        // given\n        given(ticketItemAdaptor.queryTicketItem(any())).willReturn(ticketItem);\n\n        // when\n        issuedTicketDomainService.withdrawIssuedTicket(1L, issuedTickets);\n\n        // then\n        then(ticketItem).should(times(2)).increaseQuantity(1L);\n        assertAll(\n                () ->\n                        assertEquals(\n                                issuedTicket.getIssuedTicketStatus(), IssuedTicketStatus.CANCELED),\n                () ->\n                        assertEquals(\n                                issuedTicket1.getIssuedTicketStatus(), IssuedTicketStatus.CANCELED),\n                () ->\n                        assertEquals(\n                                issuedTicket2.getIssuedTicketStatus(),\n                                IssuedTicketStatus.ENTRANCE_INCOMPLETE));\n    }\n\n    @Test\n    public void 주문중_티켓_취소_로직_정상_작동_테스트() {\n        IssuedTicketDomainService issuedTicketDomainService =\n                new IssuedTicketDomainService(\n                        issuedTicketAdaptor,\n                        ticketItemAdaptor,\n                        userAdaptor,\n                        orderAdaptor,\n                        issuedTicketValidator);\n\n        // given\n        given(issuedTicketAdaptor.findAllByOrderUuid(orderUuid)).willReturn(issuedTickets);\n        given(ticketItemAdaptor.queryTicketItem(any())).willReturn(ticketItem);\n\n        // when\n        issuedTicketDomainService.doneOrderEventAfterRollBackWithdrawIssuedTickets(1L, orderUuid);\n\n        // then\n        then(ticketItem).should(times(2)).increaseQuantity(1L);\n        assertAll(\n                () ->\n                        assertEquals(\n                                issuedTicket.getIssuedTicketStatus(), IssuedTicketStatus.CANCELED),\n                () ->\n                        assertEquals(\n                                issuedTicket1.getIssuedTicketStatus(), IssuedTicketStatus.CANCELED),\n                () ->\n                        assertEquals(\n                                issuedTicket2.getIssuedTicketStatus(),\n                                IssuedTicketStatus.ENTRANCE_INCOMPLETE));\n    }\n\n    @Test\n    public void 발급_티켓_입장_처리_로직_정상_작동_테스트() {\n        // given\n        given(issuedTicketAdaptor.queryByIssuedTicketUuid(any())).willReturn(issuedTicket1);\n        given(issuedTicketOptionAnswer.getAdditionalPrice()).willReturn(w3000);\n        given(issuedTicketOptionAnswer1.getAdditionalPrice()).willReturn(w3000);\n\n        // when\n        issuedTicketDomainService.processingEntranceIssuedTicket(1L, \"UUID\");\n\n        // then\n        assertEquals(issuedTicket1.getIssuedTicketStatus(), IssuedTicketStatus.ENTRANCE_COMPLETED);\n    }\n\n    @Test\n    public void 티켓_발급_로직_정상_작동_테스트() {\n        // given\n        given(ticketItemAdaptor.queryTicketItem(any())).willReturn(ticketItem);\n        given(userAdaptor.queryUser(any())).willReturn(user);\n        given(orderAdaptor.findByOrderUuid(any())).willReturn(order);\n        given(order.getOrderLineItems()).willReturn(orderLineItems);\n\n        // when\n        issuedTicketDomainService.createIssuedTicket(1L, orderUuid, 1L);\n\n        // then\n        then(ticketItem).should(times(orderLineItems.size())).reduceQuantity(any());\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/issuedTicket/service/handlers/OrderEventHandlerTest.java",
    "content": "package band.gosrock.domain.domains.issuedTicket.service.handlers;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyLong;\nimport static org.mockito.ArgumentMatchers.nullable;\nimport static org.mockito.BDDMockito.then;\nimport static org.mockito.Mockito.times;\n\nimport band.gosrock.domain.common.events.order.DoneOrderEvent;\nimport band.gosrock.domain.domains.issuedTicket.service.IssuedTicketDomainService;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\npublic class OrderEventHandlerTest {\n\n    @Mock IssuedTicketDomainService issuedTicketDomainService;\n\n    @Mock DoneOrderEvent doneOrderEvent;\n\n    @Mock DoneOrderEvent doneOrderEvent1;\n\n    @Mock DoneOrderEvent doneOrderEvent2;\n\n    @Test\n    public void 주문이_정상_처리되었으면_티켓발급_서비스를_실행하는지_테스트() {\n        OrderEventHandler orderEventHandler = new OrderEventHandler(issuedTicketDomainService);\n        // when\n        orderEventHandler.handleDoneOrderEvent(doneOrderEvent);\n\n        then(issuedTicketDomainService).should(times(1)).createIssuedTicket(anyLong(), nullable(String.class), anyLong());\n    }\n\n    @Test\n    public void 주문이_여러번_정상_처리되었으면_그만큼_티켓발급_서비스를_실행하는지_테스트() {\n        OrderEventHandler orderEventHandler = new OrderEventHandler(issuedTicketDomainService);\n        // when\n        orderEventHandler.handleDoneOrderEvent(doneOrderEvent);\n        orderEventHandler.handleDoneOrderEvent(doneOrderEvent1);\n        orderEventHandler.handleDoneOrderEvent(doneOrderEvent2);\n\n        then(issuedTicketDomainService).should(times(3)).createIssuedTicket(anyLong(), nullable(String.class), anyLong());\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/issuedTicket/service/handlers/WithdrawOrderEventHandlerTest.java",
    "content": "package band.gosrock.domain.domains.issuedTicket.service.handlers;\n\nimport static org.mockito.ArgumentMatchers.anyLong;\nimport static org.mockito.BDDMockito.*;\n\nimport band.gosrock.domain.common.events.order.WithDrawOrderEvent;\nimport band.gosrock.domain.domains.issuedTicket.adaptor.IssuedTicketAdaptor;\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket;\nimport band.gosrock.domain.domains.issuedTicket.service.IssuedTicketDomainService;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\npublic class WithdrawOrderEventHandlerTest {\n\n    @Mock IssuedTicketDomainService issuedTicketDomainService;\n\n    @Mock IssuedTicketAdaptor issuedTicketAdaptor;\n\n    @Mock IssuedTicket issuedTicket;\n\n    @Mock IssuedTicket issuedTicket1;\n\n    @Mock WithDrawOrderEvent withDrawOrderEvent;\n\n    List<IssuedTicket> issuedTickets = new ArrayList<>();\n\n    private String orderUuid = \"ORDERUUID\";\n\n    @BeforeEach\n    void setUp() {\n        issuedTickets.add(issuedTicket);\n        issuedTickets.add(issuedTicket1);\n    }\n\n    @Test\n    public void 주문이_취소되었으면_티켓발급_취소_로직_실행_테스트() {\n        WithdrawOrderEventHandler withDrawOrderEventHandler =\n                new WithdrawOrderEventHandler(issuedTicketDomainService, issuedTicketAdaptor);\n        // given\n        given(withDrawOrderEvent.getOrderUuid()).willReturn(orderUuid);\n        given(issuedTicketAdaptor.findAllByOrderUuid(orderUuid)).willReturn(issuedTickets);\n\n        // when\n        withDrawOrderEventHandler.handleWithdrawOrderEvent(withDrawOrderEvent);\n\n        // then\n        then(issuedTicketDomainService).should(times(1)).withdrawIssuedTicket(anyLong(), any());\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/order/domain/OrderLineItemTest.java",
    "content": "package band.gosrock.domain.domains.order.domain;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.BDDMockito.given;\n\nimport band.gosrock.domain.common.vo.Money;\nimport band.gosrock.domain.domains.cart.domain.CartLineItem;\nimport band.gosrock.domain.domains.cart.domain.CartOptionAnswer;\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem;\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\nclass OrderLineItemTest {\n\n    @Mock OrderOptionAnswer orderOptionAnswer1;\n\n    @Mock OrderOptionAnswer orderOptionAnswer2;\n    @Mock CartLineItem cartLineItem;\n    Long quantity = 2L;\n\n    @Mock TicketItem ticketItem;\n    @Mock OrderItemVo orderItem;\n\n    Money money3000 = Money.wons(3000L);\n\n    // for test\n    OrderLineItem orderLineItem;\n\n    @BeforeEach\n    void setUp() {\n\n        orderLineItem =\n                OrderLineItem.forTest(List.of(orderOptionAnswer1, orderOptionAnswer2), quantity, orderItem);\n    }\n\n    @Test\n    void 아이템_가격_조회_검증() {\n        // given\n        given(orderItem.getPrice()).willReturn(money3000);\n        // when\n        Money itemPrice = orderLineItem.getItemPrice();\n\n        // then\n        assertEquals(itemPrice, money3000);\n    }\n\n    @Test\n    void 옵션_총_가격_조회_검증() {\n        // given\n        Money optionAnswerPrice1 = Money.wons(1000L);\n        given(orderOptionAnswer1.getAdditionalPrice()).willReturn(optionAnswerPrice1);\n        Money optionAnswerPrice2 = Money.wons(2000L);\n        given(orderOptionAnswer2.getAdditionalPrice()).willReturn(optionAnswerPrice2);\n\n        // when\n        Money totalOptionAnswersPrice = orderLineItem.getOptionAnswersPrice();\n        // then\n        assertEquals(totalOptionAnswersPrice, optionAnswerPrice1.plus(optionAnswerPrice2));\n    }\n\n    @Test\n    void 오더라인_총_가격_조회_검증() {\n        // given\n        Money optionAnswerPrice1 = Money.wons(1000L);\n        given(orderOptionAnswer1.getAdditionalPrice()).willReturn(optionAnswerPrice1);\n        Money optionAnswerPrice2 = Money.wons(2000L);\n        given(orderOptionAnswer2.getAdditionalPrice()).willReturn(optionAnswerPrice2);\n        given(orderItem.getPrice()).willReturn(money3000);\n        // when\n        Money totalOrderLinePrice = orderLineItem.getTotalOrderLinePrice();\n        // then\n        Money total = optionAnswerPrice1.plus(optionAnswerPrice2).plus(money3000).times(quantity);\n        assertEquals(totalOrderLinePrice, total);\n    }\n\n    @Test\n    void 옵션에_가격이_붙으면_결제가_필요한_오더라인이다() {\n        // given\n        given(orderItem.getPrice()).willReturn(money3000);\n        Money optionAnswerPrice1 = Money.wons(1000L);\n        given(orderOptionAnswer1.getAdditionalPrice()).willReturn(optionAnswerPrice1);\n        Money optionAnswerPrice2 = Money.wons(2000L);\n        given(orderOptionAnswer2.getAdditionalPrice()).willReturn(optionAnswerPrice2);\n        // when\n        Boolean needPayment = orderLineItem.isNeedPaid();\n\n        assertTrue(needPayment);\n    }\n\n    @Test\n    void 아이템에_가격이_있으면_결제가_필요한_오더라인이다() {\n        // given\n        given(orderItem.getPrice()).willReturn(money3000);\n        given(orderOptionAnswer1.getAdditionalPrice()).willReturn(Money.ZERO);\n        given(orderOptionAnswer2.getAdditionalPrice()).willReturn(Money.ZERO);\n        // when\n        Boolean needPayment = orderLineItem.isNeedPaid();\n\n        assertTrue(needPayment);\n    }\n\n    @Test\n    void 가격이없는_오더라인이면_결제가_필요하지않다() {\n        // given\n        given(orderItem.getPrice()).willReturn(Money.ZERO);\n\n        orderLineItem =\n                OrderLineItem.forTest(List.of(orderOptionAnswer1, orderOptionAnswer2), quantity, orderItem);\n\n        given(orderOptionAnswer1.getAdditionalPrice()).willReturn(Money.ZERO);\n        given(orderOptionAnswer2.getAdditionalPrice()).willReturn(Money.ZERO);\n        // when\n        Boolean needPayment = orderLineItem.isNeedPaid();\n\n        assertFalse(needPayment);\n    }\n\n    @Test\n    public void 주문라인_아이템아이디_조회_검증() {\n        // given\n        long itemId = 1L;\n        given(orderItem.getItemId()).willReturn(itemId);\n        // when\n        Long findItemId = orderLineItem.getItemId();\n        // then\n        assertEquals(findItemId, itemId);\n    }\n\n    @Test\n    public void 주문라인_아이템그룹아이디_조회_검증() {\n        // given\n        long itemGroupId = 1L;\n        given(orderItem.getItemGroupId()).willReturn(itemGroupId);\n        // when\n        Long findItemGroupId = orderLineItem.getItemGroupId();\n        // then\n        assertEquals(findItemGroupId, itemGroupId);\n    }\n\n    @Test\n    public void 주문라인_아이템이름_조회_검증() {\n        // given\n        String name = \"아이템이름\";\n        given(orderItem.getName()).willReturn(name);\n        // when\n        String itemName = orderLineItem.getItemName();\n        // then\n        assertEquals(itemName, name);\n    }\n\n    @Test\n    public void 주문라인_정적팩터리_메서드_검증() {\n        // given\n        List<CartOptionAnswer> emptyCartOptionAnswer = List.of();\n        List<OrderOptionAnswer> emptyOrderOptionAnswers =\n                emptyCartOptionAnswer.stream().map(OrderOptionAnswer::from).toList();\n        given(cartLineItem.getCartOptionAnswers()).willReturn(emptyCartOptionAnswer);\n        given(cartLineItem.getQuantity()).willReturn(quantity);\n        // when\n        OrderLineItem build = OrderLineItem.of(cartLineItem, ticketItem);\n        // then\n        assertEquals(build.getOrderOptionAnswers(), emptyOrderOptionAnswers);\n        assertEquals(build.getQuantity(), quantity);\n    }\n\n    @Test\n    public void 주문라인_옵션아이디조회_검증() {\n        // given\n        long optionId1 = 1L;\n        given(orderOptionAnswer1.getOptionId()).willReturn(optionId1);\n        long optionId2 = 2L;\n        given(orderOptionAnswer2.getOptionId()).willReturn(optionId2);\n        List<Long> optionIds = List.of(optionId1, optionId2);\n        // when\n        List<Long> answerOptionIds = orderLineItem.getAnswerOptionIds();\n        // then\n        assertEquals(answerOptionIds, optionIds);\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/order/domain/OrderOptionAnswerTest.java",
    "content": "package band.gosrock.domain.domains.order.domain;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.BDDMockito.given;\n\nimport band.gosrock.domain.common.vo.Money;\nimport band.gosrock.domain.common.vo.OptionAnswerVo;\nimport band.gosrock.domain.domains.cart.domain.CartOptionAnswer;\nimport band.gosrock.domain.domains.ticket_item.domain.Option;\nimport band.gosrock.domain.domains.ticket_item.domain.OptionGroupType;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\nclass OrderOptionAnswerTest {\n\n    @Mock Option option;\n\n    @Mock CartOptionAnswer cartOptionAnswer;\n\n    OrderOptionAnswer orderOptionAnswer;\n\n    private final Money W3000 = Money.wons(3000L);\n    private final long optionId = 1L;\n    private final String answer = \"답변\";\n\n    @BeforeEach\n    void setUp() {\n        given(cartOptionAnswer.getAnswer()).willReturn(answer);\n        given(cartOptionAnswer.getOptionId()).willReturn(optionId);\n        given(cartOptionAnswer.getAdditionalPrice()).willReturn(W3000);\n        orderOptionAnswer = OrderOptionAnswer.from(cartOptionAnswer);\n    }\n\n    @Test\n    void 주문옵션답변_정적팩터리_생성자_검증() {\n        // given - already set up in @BeforeEach\n        // when\n        OrderOptionAnswer fromFactory = OrderOptionAnswer.from(cartOptionAnswer);\n\n        assertEquals(fromFactory.getOptionId(), optionId);\n        assertEquals(fromFactory.getAnswer(), answer);\n        assertEquals(fromFactory.getAdditionalPrice(), W3000);\n    }\n\n    @Test\n    void 주문옵션답변_옵션답변정보_변환_검증() {\n        // given\n        String questionName = \"질문이름\";\n        String questionDescription = \"질문설명\";\n        OptionGroupType trueFalse = OptionGroupType.TRUE_FALSE;\n        given(option.getQuestionDescription()).willReturn(questionDescription);\n        given(option.getQuestionType()).willReturn(trueFalse);\n        given(option.getQuestionName()).willReturn(questionName);\n        OptionAnswerVo build =\n                new OptionAnswerVo(trueFalse, questionName, questionDescription, answer, W3000);\n        // when\n        OptionAnswerVo optionAnswerVo = orderOptionAnswer.getOptionAnswerVo(option);\n        assertEquals(optionAnswerVo, build);\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/order/domain/OrderTest.java",
    "content": "package band.gosrock.domain.domains.order.domain;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.BDDMockito.given;\n\nimport band.gosrock.domain.common.vo.Money;\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\nclass OrderTest {\n\n    @Mock OrderCouponVo orderCouponVo;\n\n    @Mock OrderLineItem orderLineItem1;\n\n    @Mock OrderLineItem orderLineItem2;\n\n    // 조회용테스트 order\n    Order notHaveCouponOrder;\n\n    Order couponOrder;\n\n    @BeforeEach\n    void setUp() {\n        notHaveCouponOrder =\n                Order.forTest(1L, \"주문이름\", List.of(orderLineItem1, orderLineItem2), OrderStatus.PENDING_APPROVE, OrderMethod.APPROVAL, null);\n        couponOrder =\n                Order.forTest(1L, \"주문이름\", List.of(orderLineItem1, orderLineItem2), OrderStatus.PENDING_APPROVE, OrderMethod.APPROVAL, null);\n        couponOrder.attachCoupon(orderCouponVo);\n    }\n\n    @Test\n    void 할인쿠폰이없다면_총할인금액은_0원이다() {\n        // given\n        // when\n        Money totalDiscountPrice = notHaveCouponOrder.getTotalDiscountPrice();\n        // then\n        assertEquals(totalDiscountPrice, Money.ZERO);\n    }\n\n    @Test\n    void 총공급가액_계산_검증() {\n        // given\n        Money wons2000 = Money.wons(3000L);\n        Money wons4000 = Money.wons(4000L);\n        given(orderLineItem1.getTotalOrderLinePrice()).willReturn(wons2000);\n        given(orderLineItem2.getTotalOrderLinePrice()).willReturn(wons4000);\n        // when\n        Money totalSupplyPrice = notHaveCouponOrder.getTotalSupplyPrice();\n        // then\n        assertEquals(totalSupplyPrice, wons2000.plus(wons4000));\n    }\n\n    @Test\n    void 쿠폰_없을때_총결제금액_계산_검증() {\n        // given\n        Money wons2000 = Money.wons(3000L);\n        Money wons4000 = Money.wons(4000L);\n        given(orderLineItem1.getTotalOrderLinePrice()).willReturn(wons2000);\n        given(orderLineItem2.getTotalOrderLinePrice()).willReturn(wons4000);\n        // when\n        Money totalPaymentPrice = notHaveCouponOrder.getTotalPaymentPrice();\n        // then\n        assertEquals(totalPaymentPrice, wons2000.plus(wons4000));\n    }\n\n    @Test\n    void 쿠폰_있을때_총결제금액_계산_검증() {\n        // given\n        Money wons3000 = Money.wons(3000L);\n        Money wons4000 = Money.wons(4000L);\n        given(orderLineItem1.getTotalOrderLinePrice()).willReturn(wons3000);\n        given(orderLineItem2.getTotalOrderLinePrice()).willReturn(wons4000);\n        given(orderCouponVo.getDiscountAmount()).willReturn(wons4000);\n\n        // when\n        Money totalPaymentPrice = couponOrder.getTotalPaymentPrice();\n        // then\n        assertEquals(totalPaymentPrice, wons3000.plus(wons4000).minus(wons4000));\n    }\n\n    @Test\n    void 쿠폰_으로_0원_결제가능() {\n        // given\n        Money wons3000 = Money.wons(3000L);\n        Money wons4000 = Money.wons(4000L);\n        given(orderLineItem1.getTotalOrderLinePrice()).willReturn(wons3000);\n        given(orderLineItem2.getTotalOrderLinePrice()).willReturn(wons4000);\n        given(orderCouponVo.getDiscountAmount()).willReturn(wons4000.plus(wons3000));\n\n        // when\n        Money totalPaymentPrice = couponOrder.getTotalPaymentPrice();\n        // then\n        assertEquals(totalPaymentPrice, Money.ZERO);\n        assertFalse(couponOrder.isNeedPaid());\n    }\n\n    @Test\n    void 주문번호생성_검증() {\n        // given\n        Money wons3000 = Money.wons(3000L);\n        Money wons4000 = Money.wons(4000L);\n        given(orderLineItem1.getTotalOrderLinePrice()).willReturn(wons3000);\n        given(orderLineItem2.getTotalOrderLinePrice()).willReturn(wons4000);\n        given(orderCouponVo.getDiscountAmount()).willReturn(wons4000.plus(wons3000));\n\n        // when\n        Money totalPaymentPrice = couponOrder.getTotalPaymentPrice();\n        // then\n        assertEquals(totalPaymentPrice, Money.ZERO);\n        assertFalse(couponOrder.isNeedPaid());\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/order/domain/validator/OrderValidatorTest.java",
    "content": "package band.gosrock.domain.domains.order.domain.validator;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.BDDMockito.willCallRealMethod;\nimport static org.mockito.BDDMockito.willDoNothing;\nimport static org.mockito.BDDMockito.willThrow;\n\nimport band.gosrock.domain.common.vo.Money;\nimport band.gosrock.domain.domains.event.adaptor.EventAdaptor;\nimport band.gosrock.domain.domains.event.domain.Event;\nimport band.gosrock.domain.domains.event.exception.EventNotOpenException;\nimport band.gosrock.domain.domains.event.exception.EventTicketingTimeIsPassedException;\nimport band.gosrock.domain.domains.issuedTicket.adaptor.IssuedTicketAdaptor;\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor;\nimport band.gosrock.domain.domains.order.domain.Order;\nimport band.gosrock.domain.domains.order.domain.OrderLineItem;\nimport band.gosrock.domain.domains.order.domain.OrderMethod;\nimport band.gosrock.domain.domains.order.domain.OrderStatus;\nimport band.gosrock.domain.domains.order.exception.CanNotCancelOrderException;\nimport band.gosrock.domain.domains.order.exception.CanNotRefundOrderException;\nimport band.gosrock.domain.domains.order.exception.InvalidOrderException;\nimport band.gosrock.domain.domains.order.exception.NotApprovalOrderException;\nimport band.gosrock.domain.domains.order.exception.NotFreeOrderException;\nimport band.gosrock.domain.domains.order.exception.NotOwnerOrderException;\nimport band.gosrock.domain.domains.order.exception.NotPaymentOrderException;\nimport band.gosrock.domain.domains.order.exception.NotPendingOrderException;\nimport band.gosrock.domain.domains.order.exception.NotRefundAvailableDateOrderException;\nimport band.gosrock.domain.domains.order.exception.OrdeItemNotOneTypeException;\nimport band.gosrock.domain.domains.order.exception.OrderItemOptionChangedException;\nimport band.gosrock.domain.domains.ticket_item.adaptor.OptionAdaptor;\nimport band.gosrock.domain.domains.ticket_item.adaptor.TicketItemAdaptor;\nimport band.gosrock.domain.domains.ticket_item.domain.Option;\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem;\nimport band.gosrock.domain.domains.ticket_item.exception.TicketItemQuantityLackException;\nimport band.gosrock.domain.domains.ticket_item.exception.TicketPurchaseLimitException;\nimport band.gosrock.domain.domains.user.adaptor.UserAdaptor;\nimport java.util.Arrays;\nimport java.util.List;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.junit.jupiter.api.function.Executable;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\nclass OrderValidatorTest {\n\n    @Mock Order order;\n\n    @Mock Event event;\n    @Mock Event availableRefundEvent;\n    @Mock Event unavailableRefundEvent;\n    @Mock EventAdaptor eventAdaptor;\n    @Mock TicketItemAdaptor ticketItemAdaptor;\n    @Mock IssuedTicketAdaptor issuedTicketAdaptor;\n    @Mock OptionAdaptor optionAdaptor;\n    @Mock UserAdaptor userAdaptor;\n    @Mock OrderAdaptor OrderAdaptor;\n    @Mock Option optionOfGroup1;\n    @Mock Option optionOfGroup2;\n    @Mock OrderLineItem orderLineItem;\n\n    @Mock TicketItem item;\n\n    OrderValidator orderValidator;\n\n    @BeforeEach\n    void setUp() {\n        orderValidator =\n                new OrderValidator(\n                        eventAdaptor,\n                        ticketItemAdaptor,\n                        issuedTicketAdaptor,\n                        optionAdaptor,\n                        userAdaptor,\n                        OrderAdaptor);\n    }\n\n    @Test\n    public void 주문방식_승인주문_검증_실패() {\n        // given\n        given(order.getOrderMethod()).willReturn(OrderMethod.PAYMENT);\n        // when\n        // then\n        assertThrows(\n                NotApprovalOrderException.class,\n                () -> orderValidator.validMethodIsCanApprove(order));\n    }\n\n    @Test\n    public void 주문방식_승인주문_검증_성공() {\n        // given\n        given(order.getOrderMethod()).willReturn(OrderMethod.APPROVAL);\n        // when\n        orderValidator.validMethodIsCanApprove(order);\n        // then\n    }\n\n    @Test\n    public void 주인_검증_실패() {\n        // given\n        long userId = 1L;\n        long otherUserId = 2L;\n        given(order.getUserId()).willReturn(otherUserId);\n        // when\n        // then\n        assertThrows(NotOwnerOrderException.class, () -> orderValidator.validOwner(order, userId));\n    }\n\n    @Test\n    public void 주인_검증_성공() {\n        // given\n        long userId = 1L;\n        given(order.getUserId()).willReturn(userId);\n        // when\n        orderValidator.validOwner(order, userId);\n        // then\n    }\n\n    @Test\n    public void 주문확인_금액검증_성공() {\n        // given\n        Money won3000 = Money.wons(3000L);\n        given(order.getTotalPaymentPrice()).willReturn(won3000);\n        // when\n        orderValidator.validAmountIsSameAsRequest(order, won3000);\n        // then\n    }\n\n    @Test\n    public void 주문확인_금액검증_실패() {\n        // given\n        Money won3000 = Money.wons(3000L);\n        Money won2000 = Money.wons(2000L);\n        given(order.getTotalPaymentPrice()).willReturn(won3000);\n        // when\n        // then\n        assertThrows(\n                InvalidOrderException.class,\n                () -> orderValidator.validAmountIsSameAsRequest(order, won2000));\n    }\n\n    @Test\n    public void 주문방식_결제방식검증_성공() {\n        // given\n        given(order.getOrderMethod()).willReturn(OrderMethod.PAYMENT);\n\n        // when\n        orderValidator.validMethodIsPaymentOrder(order);\n        // then\n    }\n\n    @Test\n    public void 주문방식_결제방식검증_실패() {\n        // given\n        given(order.getOrderMethod()).willReturn(OrderMethod.APPROVAL);\n        // when\n        // then\n        assertThrows(\n                NotPaymentOrderException.class,\n                () -> orderValidator.validMethodIsPaymentOrder(order));\n    }\n\n    @Test\n    public void 주문환불_환불가능상태검증_실패() {\n        // given\n        given(availableRefundEvent.isRefundDateNotPassed()).willReturn(Boolean.TRUE);\n        given(unavailableRefundEvent.isRefundDateNotPassed()).willReturn(Boolean.FALSE);\n        given(eventAdaptor.findAllByIds(any()))\n                .willReturn(List.of(availableRefundEvent, unavailableRefundEvent));\n        // when\n        // then\n        assertThrows(\n                NotRefundAvailableDateOrderException.class,\n                () -> orderValidator.validAvailableRefundDate(order));\n    }\n\n    @Test\n    public void 주문환불_환불가능상태검증_성공() {\n        // given\n        given(availableRefundEvent.isRefundDateNotPassed()).willReturn(Boolean.TRUE);\n        given(eventAdaptor.findAllByIds(any())).willReturn(List.of(availableRefundEvent));\n        // when\n        orderValidator.validAvailableRefundDate(order);\n        // then\n    }\n\n    @Test\n    public void 주문_무료금액검증_성공() {\n        // given\n        given(order.isNeedPaid()).willReturn(Boolean.FALSE);\n        // when\n        orderValidator.validAmountIsFree(order);\n        // then\n    }\n\n    @Test\n    public void 주문_무료금액검증_실패() {\n        // given\n        given(order.isNeedPaid()).willReturn(Boolean.TRUE);\n        // when\n        // then\n        assertThrows(NotFreeOrderException.class, () -> orderValidator.validAmountIsFree(order));\n    }\n\n    @Test\n    public void 주문취소_상태검증_실패() {\n        // given\n        List<Executable> executables =\n                Arrays.stream(OrderStatus.values())\n                        .filter(orderStatus -> !orderValidator.isStatusCanWithDraw(orderStatus))\n                        .<Executable>map(\n                                orderStatus ->\n                                        () -> orderValidator.validStatusCanCancel(orderStatus))\n                        .toList();\n        // then\n        executables.forEach(\n                executable -> assertThrows(CanNotCancelOrderException.class, executable));\n    }\n\n    @Test\n    public void 주문취소_상태검증_성공() {\n        // given\n        OrderStatus confirm = OrderStatus.CONFIRM;\n        OrderStatus approved = OrderStatus.APPROVED;\n        // when\n        orderValidator.validStatusCanCancel(confirm);\n        orderValidator.validStatusCanCancel(approved);\n        // then\n    }\n\n    @Test\n    public void 주문환불_상태검증_실패() {\n        // given\n        List<Executable> executables =\n                Arrays.stream(OrderStatus.values())\n                        .filter(orderStatus -> !orderValidator.isStatusCanWithDraw(orderStatus))\n                        .<Executable>map(\n                                orderStatus ->\n                                        () -> orderValidator.validStatusCanRefund(orderStatus))\n                        .toList();\n        // then\n        executables.forEach(\n                executable -> assertThrows(CanNotRefundOrderException.class, executable));\n    }\n\n    @Test\n    public void 주문환불_상태검증_성공() {\n        // given\n        OrderStatus confirm = OrderStatus.CONFIRM;\n        OrderStatus approved = OrderStatus.APPROVED;\n        // when\n        orderValidator.validStatusCanRefund(confirm);\n        orderValidator.validStatusCanRefund(approved);\n        // then\n    }\n\n    @Test\n    public void 주문확인_결제대기중검증_성공() {\n        // given\n        OrderStatus pendingPayment = OrderStatus.PENDING_PAYMENT;\n        // when\n        orderValidator.validStatusCanPaymentConfirm(pendingPayment);\n        // then\n    }\n\n    @Test\n    public void 주문확인_결제대기중검증_실패() {\n        // given\n        // when\n        List<Executable> executables =\n                Arrays.stream(OrderStatus.values())\n                        .filter(orderStatus -> !(OrderStatus.PENDING_PAYMENT == orderStatus))\n                        .<Executable>map(\n                                orderStatus ->\n                                        () ->\n                                                orderValidator.validStatusCanPaymentConfirm(\n                                                        orderStatus))\n                        .toList();\n        // then\n        executables.forEach(executable -> assertThrows(NotPendingOrderException.class, executable));\n    }\n\n    @Test\n    public void 주문확인_승인대기중검증_성공() {\n        // given\n        OrderStatus pendingApprove = OrderStatus.PENDING_APPROVE;\n        // when\n        orderValidator.validStatusCanApprove(pendingApprove);\n        // then\n    }\n\n    @Test\n    public void 주문확인_승인대기중검증_실패() {\n        // given\n        // when\n        List<Executable> executables =\n                Arrays.stream(OrderStatus.values())\n                        .filter(orderStatus -> !(OrderStatus.PENDING_APPROVE == orderStatus))\n                        .<Executable>map(\n                                orderStatus ->\n                                        () -> orderValidator.validStatusCanApprove(orderStatus))\n                        .toList();\n        // then\n        executables.forEach(executable -> assertThrows(NotPendingOrderException.class, executable));\n    }\n\n    @Test\n    public void 주문과정중_상품옵션이_변하면_실패() {\n        // given\n        given(order.getOrderLineItems()).willReturn(List.of(orderLineItem));\n        given(optionAdaptor.findAllByIds(any()))\n                .willReturn(List.of(optionOfGroup1, optionOfGroup2));\n        Long optionGroup1Id = 1L;\n        Long optionGroup2Id = 2L;\n\n        given(optionOfGroup1.getOptionGroupId()).willReturn(optionGroup1Id);\n        given(optionOfGroup2.getOptionGroupId()).willReturn(optionGroup2Id);\n        Long optionGroup3Id = 3L;\n        given(item.getOptionGroupIds())\n                .willReturn(List.of(optionGroup1Id, optionGroup2Id, optionGroup3Id));\n        // when\n        // then\n        assertThrows(\n                OrderItemOptionChangedException.class,\n                () -> orderValidator.validOptionNotChange(order, item));\n    }\n\n    @Test\n    public void 주문과정중_상품옵션이_그대로면_성공() {\n        // given\n        given(order.getOrderLineItems()).willReturn(List.of(orderLineItem));\n        given(optionAdaptor.findAllByIds(any()))\n                .willReturn(List.of(optionOfGroup1, optionOfGroup2));\n\n        Long optionGroup1Id = 1L;\n        Long optionGroup2Id = 2L;\n        given(optionOfGroup1.getOptionGroupId()).willReturn(optionGroup1Id);\n        given(optionOfGroup2.getOptionGroupId()).willReturn(optionGroup2Id);\n\n        given(item.getOptionGroupIds()).willReturn(List.of(optionGroup1Id, optionGroup2Id));\n        // when\n        orderValidator.validOptionNotChange(order, item);\n        // then\n    }\n\n    @Test\n    public void 주문_티켓팅_가능시간검증_실패() {\n        // given\n        given(event.isTimeBeforeStartAt()).willReturn(Boolean.FALSE);\n        willCallRealMethod().given(event).validateTicketingTime();\n        // when\n        // then\n        assertThrows(\n                EventTicketingTimeIsPassedException.class,\n                () -> orderValidator.validTicketingTime(event));\n    }\n\n    @Test\n    public void 주문_티켓팅_가능시간검증_성공() {\n        // given\n        given(event.isTimeBeforeStartAt()).willReturn(Boolean.TRUE);\n        willCallRealMethod().given(event).validateTicketingTime();\n        // when\n        orderValidator.validTicketingTime(event);\n        // then\n    }\n\n    @Test\n    public void 주문_티켓팅_재고검증_실패() {\n        // given\n        willThrow(TicketItemQuantityLackException.class).given(item).validEnoughQuantity(any());\n        // when\n        // then\n        assertThrows(\n                TicketItemQuantityLackException.class,\n                () -> orderValidator.validItemStockEnough(order, item));\n    }\n\n    @Test\n    public void 주문_티켓팅_재고검증_성공() {\n        // given\n        willDoNothing().given(item).validEnoughQuantity(any());\n        // when\n        orderValidator.validItemStockEnough(order, item);\n        // then\n    }\n\n    @Test\n    public void 주문_티켓팅_이벤트_상태검증_성공() {\n        // given\n        willDoNothing().given(event).validateNotOpenStatus();\n        // when\n        orderValidator.validEventIsOpen(event);\n        // then\n    }\n\n    @Test\n    public void 주문_티켓팅_이벤트_상태검증_실패() {\n        // given\n        willThrow(EventNotOpenException.class).given(event).validateNotOpenStatus();\n        // when\n        // then\n        assertThrows(EventNotOpenException.class, () -> orderValidator.validEventIsOpen(event));\n    }\n\n    @Test\n    public void 주문_아이템_한종류가아니면_실패() {\n        // given\n        given(order.getDistinctItemIds()).willReturn(List.of(1L, 2L));\n        // then\n        assertThrows(\n                OrdeItemNotOneTypeException.class,\n                () -> orderValidator.validItemKindIsOneType(order));\n    }\n\n    @Test\n    public void 주문_아이템_한종류면_성공() {\n        // given\n        given(order.getDistinctItemIds()).willReturn(List.of(2L));\n        // then\n        orderValidator.validItemKindIsOneType(order);\n    }\n\n    @Test\n    public void 주문_아이템_구매갯수제한_실패() {\n        // given\n        given(order.getTotalQuantity()).willReturn(3L);\n        willThrow(TicketPurchaseLimitException.EXCEPTION).given(item).validPurchaseLimit(any());\n        // then\n        assertThrows(\n                TicketPurchaseLimitException.class,\n                () -> orderValidator.validItemPurchaseLimit(order, item));\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/order/service/OrderApproveServiceConcurrencyFailTest.java",
    "content": "package band.gosrock.domain.domains.order.service;\n\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.BDDMockito.willCallRealMethod;\nimport static org.mockito.BDDMockito.willDoNothing;\n\nimport band.gosrock.domain.CunCurrencyExecutorService;\nimport band.gosrock.domain.DisableDomainEvent;\nimport band.gosrock.domain.DisableRedissonLock;\nimport band.gosrock.domain.DomainIntegrateSpringBootTest;\nimport band.gosrock.domain.common.vo.Money;\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor;\nimport band.gosrock.domain.domains.order.domain.Order;\nimport band.gosrock.domain.domains.order.domain.OrderLineItem;\nimport band.gosrock.domain.domains.order.domain.OrderMethod;\nimport band.gosrock.domain.domains.order.domain.OrderStatus;\nimport band.gosrock.domain.domains.order.domain.validator.OrderValidator;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.Mock;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.mock.mockito.MockBean;\n\n@DomainIntegrateSpringBootTest\n@DisableDomainEvent\n@DisableRedissonLock\n@Slf4j\nclass OrderApproveServiceConcurrencyFailTest {\n    @Autowired OrderApproveService orderApproveService;\n\n    @Mock OrderLineItem orderLineItem;\n\n    @MockBean OrderAdaptor orderAdaptor;\n    @MockBean OrderValidator orderValidator;\n\n    Order order;\n\n    @BeforeEach\n    void setUp() {\n        given(orderLineItem.getTotalOrderLinePrice()).willReturn(Money.ZERO);\n        order =\n                Order.forTest(1L, null, List.of(orderLineItem), OrderStatus.PENDING_APPROVE, OrderMethod.APPROVAL, null);\n        order.addUUID();\n        willDoNothing().given(orderValidator).validCanDone(any());\n        willDoNothing().given(orderValidator).validUserNotDeleted(any());\n        willCallRealMethod().given(orderValidator).validCanApproveOrder(any());\n        willCallRealMethod().given(orderValidator).validStatusCanApprove(any());\n        given(orderAdaptor.findByOrderUuid(any())).willReturn(order);\n    }\n\n    @Test\n    @DisplayName(\"락을 적용안하면 동시에 여러 주문요청이 들어왔을 때 중복 승인이 되어야한다.\")\n    void 동시성_실패_주문승인() throws InterruptedException {\n        // given\n        // when\n        AtomicLong successCount = new AtomicLong();\n        CunCurrencyExecutorService.execute(\n                () -> orderApproveService.execute(order.getUuid()), successCount);\n\n        // then\n        // 가끔 동시요청이 ci 환경에서 중복안될때가 있음 로그로 확인하셈!\n        log.info(String.valueOf(successCount.get()));\n        assertThat(successCount.get()).isGreaterThanOrEqualTo(1);\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/order/service/OrderApproveServiceConcurrencyTest.java",
    "content": "package band.gosrock.domain.domains.order.service;\n\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.BDDMockito.willCallRealMethod;\nimport static org.mockito.BDDMockito.willDoNothing;\n\nimport band.gosrock.domain.CunCurrencyExecutorService;\nimport band.gosrock.domain.DisableDomainEvent;\nimport band.gosrock.domain.DomainIntegrateSpringBootTest;\nimport band.gosrock.domain.common.vo.Money;\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor;\nimport band.gosrock.domain.domains.order.domain.Order;\nimport band.gosrock.domain.domains.order.domain.OrderLineItem;\nimport band.gosrock.domain.domains.order.domain.OrderMethod;\nimport band.gosrock.domain.domains.order.domain.OrderStatus;\nimport band.gosrock.domain.domains.order.domain.validator.OrderValidator;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.Mock;\nimport org.redisson.api.RedissonClient;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.mock.mockito.MockBean;\n\n@DomainIntegrateSpringBootTest\n@DisableDomainEvent\n@Slf4j\nclass OrderApproveServiceConcurrencyTest {\n\n    @Autowired private OrderApproveService orderApproveService;\n\n    @Autowired RedissonClient redissonClient;\n\n    @Mock OrderLineItem orderLineItem;\n    @MockBean private OrderAdaptor orderAdaptor;\n    @MockBean private OrderValidator orderValidator;\n\n    Order order;\n\n    @BeforeEach\n    void setUp() {\n        given(orderLineItem.getTotalOrderLinePrice()).willReturn(Money.ZERO);\n        order =\n                Order.forTest(1L, null, List.of(orderLineItem), OrderStatus.PENDING_APPROVE, OrderMethod.APPROVAL, null);\n        order.addUUID();\n        willDoNothing().given(orderValidator).validCanDone(any());\n        willDoNothing().given(orderValidator).validUserNotDeleted(any());\n        willCallRealMethod().given(orderValidator).validCanApproveOrder(any());\n        willCallRealMethod().given(orderValidator).validStatusCanApprove(any());\n        given(orderAdaptor.findByOrderUuid(any())).willReturn(order);\n    }\n\n    @Test\n    @DisplayName(\"동시에 주문 승인 요청이 와도 하나의 요청만 성공해야한다.\")\n    void 동시성_주문승인() throws InterruptedException {\n        // given\n        // when\n        AtomicLong successCount = new AtomicLong();\n        CunCurrencyExecutorService.execute(\n                () -> orderApproveService.execute(order.getUuid()), successCount);\n        // then\n        assertThat(successCount.get()).isEqualTo(1);\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/order/service/OrderApproveServiceTest.java",
    "content": "package band.gosrock.domain.domains.order.service;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.BDDMockito.then;\nimport static org.mockito.BDDMockito.willDoNothing;\nimport static org.mockito.Mockito.times;\n\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor;\nimport band.gosrock.domain.domains.order.domain.Order;\nimport band.gosrock.domain.domains.order.domain.validator.OrderValidator;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\nclass OrderApproveServiceTest {\n\n    @Mock private OrderAdaptor orderAdaptor;\n\n    @Mock private Order order;\n\n    @Mock private OrderValidator orderValidator;\n\n    @Test\n    @DisplayName(\"주문승인_승인로직_한번만_호출해야한다.\")\n    void 주문승인_승인로직_한번만_호출() {\n        // given\n        willDoNothing().given(order).approve(orderValidator);\n        given(orderAdaptor.findByOrderUuid(any())).willReturn(order);\n        OrderApproveService orderApproveService =\n                new OrderApproveService(orderAdaptor, orderValidator);\n\n        // when\n        orderApproveService.execute(\"uuid\");\n\n        // then\n        then(order).should(times(1)).approve(any());\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/order/service/WithdrawOrderServiceTest.java",
    "content": "package band.gosrock.domain.domains.order.service;\n\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.BDDMockito.willDoNothing;\n\nimport band.gosrock.domain.CunCurrencyExecutorService;\nimport band.gosrock.domain.DisableDomainEvent;\nimport band.gosrock.domain.DomainIntegrateSpringBootTest;\nimport band.gosrock.domain.common.vo.Money;\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor;\nimport band.gosrock.domain.domains.order.domain.Order;\nimport band.gosrock.domain.domains.order.domain.OrderLineItem;\nimport band.gosrock.domain.domains.order.domain.OrderMethod;\nimport band.gosrock.domain.domains.order.domain.OrderStatus;\nimport band.gosrock.domain.domains.order.domain.validator.OrderValidator;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicLong;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.Mock;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.mock.mockito.MockBean;\n\n@DomainIntegrateSpringBootTest\n@DisableDomainEvent\n@Slf4j\nclass WithdrawOrderServiceTest {\n\n    @Autowired WithdrawOrderService withdrawOrderService;\n\n    @MockBean OrderAdaptor orderAdaptor;\n    @Mock OrderLineItem orderLineItem;\n\n    @MockBean OrderValidator orderValidator;\n    Order order;\n    static Long userId = 1L;\n\n    @BeforeEach\n    void setUp() {\n        order =\n                Order.forTest(userId, null, List.of(orderLineItem), OrderStatus.CONFIRM, OrderMethod.PAYMENT, null);\n        order.addUUID();\n        given(orderAdaptor.findByOrderUuid(any())).willReturn(order);\n        given(orderLineItem.getTotalOrderLinePrice()).willReturn(Money.ZERO);\n        willDoNothing().given(orderValidator).validAvailableRefundDate(any());\n    }\n\n    @Test\n    void 동시성_주문철회_취소케이스() throws InterruptedException {\n        // given\n        AtomicLong successCount = new AtomicLong();\n        // when\n        CunCurrencyExecutorService.execute(\n                () -> withdrawOrderService.cancelOrder(order.getUuid()), successCount);\n\n        assertThat(successCount.get()).isGreaterThanOrEqualTo(1);\n    }\n\n    @Test\n    void 동시성_주문철회_환불케이스() throws InterruptedException {\n        // given\n        AtomicLong successCount = new AtomicLong();\n        // when\n        CunCurrencyExecutorService.execute(\n                () -> withdrawOrderService.refundOrder(order.getUuid(), userId), successCount);\n\n        assertThat(successCount.get()).isGreaterThanOrEqualTo(1);\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domain/domains/order/service/handler/WithDrawOrderHandlerTest.java",
    "content": "package band.gosrock.domain.domains.order.service.handler;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.BDDMockito.then;\nimport static org.mockito.Mockito.times;\n\nimport band.gosrock.domain.common.events.order.WithDrawOrderEvent;\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor;\nimport band.gosrock.domain.domains.order.domain.Order;\nimport band.gosrock.domain.domains.order.domain.OrderStatus;\nimport band.gosrock.domain.domains.order.service.WithdrawPaymentService;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\n@ExtendWith(MockitoExtension.class)\nclass WithDrawOrderHandlerTest {\n\n    @Mock OrderAdaptor orderAdaptor;\n\n    @Mock WithdrawPaymentService withdrawPaymentService;\n\n    @Mock WithDrawOrderEvent withDrawOrderEvent;\n\n    @Mock Order order;\n\n    @Test\n    public void 결제된_주문이_아니면_토스로_철회요청을_보내지_않는다() {\n        // given\n        WithDrawOrderHandler withDrawOrderHandler =\n                new WithDrawOrderHandler(withdrawPaymentService, orderAdaptor);\n        given(order.isPaid()).willReturn(Boolean.FALSE);\n        given(orderAdaptor.findByOrderUuid(any())).willReturn(order);\n        // when\n        withDrawOrderHandler.handleWithDrawOrderEvent(withDrawOrderEvent);\n        // then\n        then(withdrawPaymentService).should(times(0)).execute(any(), any(), any());\n    }\n\n    @Test\n    public void 결제된_주문이면_토스로_철회요청을_보낸다() {\n        // given\n        WithDrawOrderHandler withDrawOrderHandler =\n                new WithDrawOrderHandler(withdrawPaymentService, orderAdaptor);\n        given(order.isPaid()).willReturn(Boolean.TRUE);\n        given(orderAdaptor.findByOrderUuid(any())).willReturn(order);\n        given(withDrawOrderEvent.getOrderStatus()).willReturn(OrderStatus.CANCELED);\n        // when\n        withDrawOrderHandler.handleWithDrawOrderEvent(withDrawOrderEvent);\n        // then\n        then(withdrawPaymentService).should(times(1)).execute(any(), any(), any());\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domains/user/domain/OauthInfoTest.java",
    "content": "package band.gosrock.domains.user.domain;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport band.gosrock.common.consts.DuDoongStatic;\nimport band.gosrock.domain.domains.user.domain.OauthInfo;\nimport band.gosrock.domain.domains.user.domain.OauthProvider;\nimport org.junit.jupiter.api.Test;\n\nclass OauthInfoTest {\n\n    @Test\n    public void 탈퇴시에_OauthInfo_oid가_탈퇴상태가되어야한다() {\n        // given\n        String testOid = \"test\";\n        String withDrawOid = DuDoongStatic.WITHDRAW_PREFIX + testOid;\n        OauthInfo oauthInfo =\n                new OauthInfo(OauthProvider.KAKAO, testOid);\n\n        // when\n        OauthInfo withDrawOauthInfo = oauthInfo.withDrawOauthInfo();\n\n        // then\n        assertTrue(withDrawOauthInfo.getOid().startsWith(DuDoongStatic.WITHDRAW_PREFIX));\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/java/band/gosrock/domains/user/domain/UserTest.java",
    "content": "package band.gosrock.domains.user.domain;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport band.gosrock.domain.domains.user.domain.Profile;\nimport band.gosrock.domain.domains.user.domain.User;\nimport org.junit.jupiter.api.Test;\n\nclass UserTest {\n\n    @Test\n    public void 유저프로필변경테스트() {\n\n        // given\n        Profile profile = new Profile(\"곽팔두\", \"t@naver.com\", null, null);\n        User user = new User(profile, null, false);\n        Profile newProfile = new Profile(\"홍길동\", \"a@naver.com\", null, null);\n        // when\n        user.changeProfile(newProfile);\n        // then\n\n        assertEquals(newProfile.getEmail(), user.getProfile().getEmail());\n        assertEquals(newProfile.getName(), user.getProfile().getName());\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/kotlin/band/gosrock/domain/common/vo/DateTimePeriodTest.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport org.junit.jupiter.api.Assertions.assertFalse\nimport org.junit.jupiter.api.Assertions.assertTrue\nimport org.junit.jupiter.api.Test\nimport java.time.LocalDateTime\n\nclass DateTimePeriodTest {\n\n    private val start: LocalDateTime = LocalDateTime.of(2025, 1, 1, 10, 0)\n    private val end: LocalDateTime = LocalDateTime.of(2025, 1, 1, 18, 0)\n\n    @Test\n    fun `기간 내 시간은 contains가 true를 반환한다`() {\n        val period = DateTimePeriod.between(start, end)\n        val middle = LocalDateTime.of(2025, 1, 1, 14, 0)\n        assertTrue(period.contains(middle))\n    }\n\n    @Test\n    fun `시작 시각과 정확히 같으면 contains가 true를 반환한다`() {\n        val period = DateTimePeriod.between(start, end)\n        assertTrue(period.contains(start))\n    }\n\n    @Test\n    fun `종료 시각과 정확히 같으면 contains가 true를 반환한다`() {\n        val period = DateTimePeriod.between(start, end)\n        assertTrue(period.contains(end))\n    }\n\n    @Test\n    fun `시작 시각 이전이면 contains가 false를 반환한다`() {\n        val period = DateTimePeriod.between(start, end)\n        val before = LocalDateTime.of(2025, 1, 1, 9, 59)\n        assertFalse(period.contains(before))\n    }\n\n    @Test\n    fun `종료 시각 이후이면 contains가 false를 반환한다`() {\n        val period = DateTimePeriod.between(start, end)\n        val after = LocalDateTime.of(2025, 1, 1, 18, 1)\n        assertFalse(period.contains(after))\n    }\n\n    @Test\n    fun `startAt이 null이면 contains가 false를 반환한다`() {\n        val period = DateTimePeriod.between(null, end)\n        assertFalse(period.contains(start))\n    }\n\n    @Test\n    fun `endAt이 null이면 contains가 false를 반환한다`() {\n        val period = DateTimePeriod.between(start, null)\n        assertFalse(period.contains(start))\n    }\n\n    @Test\n    fun `startAt과 endAt이 모두 null이면 contains가 false를 반환한다`() {\n        val period = DateTimePeriod.between(null, null)\n        assertFalse(period.contains(LocalDateTime.now()))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/kotlin/band/gosrock/domain/common/vo/MoneyTest.kt",
    "content": "package band.gosrock.domain.common.vo\n\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Assertions.assertFalse\nimport org.junit.jupiter.api.Assertions.assertNotEquals\nimport org.junit.jupiter.api.Assertions.assertTrue\nimport org.junit.jupiter.api.Test\n\nclass MoneyTest {\n\n    @Test\n    fun `ZERO는 0원이다`() {\n        assertEquals(0L, Money.ZERO.longValue())\n    }\n\n    @Test\n    fun `wons(Long)으로 금액을 생성한다`() {\n        val money = Money.wons(5000L)\n        assertEquals(5000L, money.longValue())\n    }\n\n    @Test\n    fun `wons(Double)으로 금액을 생성한다`() {\n        val money = Money.wons(1500.0)\n        assertEquals(1500L, money.longValue())\n    }\n\n    @Test\n    fun `두 금액을 더하면 합산된 금액이 반환된다`() {\n        val a = Money.wons(3000L)\n        val b = Money.wons(2000L)\n        assertEquals(Money.wons(5000L), a.plus(b))\n    }\n\n    @Test\n    fun `두 금액을 빼면 차감된 금액이 반환된다`() {\n        val a = Money.wons(5000L)\n        val b = Money.wons(2000L)\n        assertEquals(Money.wons(3000L), a.minus(b))\n    }\n\n    @Test\n    fun `금액에 배수를 곱하면 곱셈 결과가 반환된다`() {\n        val a = Money.wons(1000L)\n        assertEquals(Money.wons(2000L), a.times(2.0))\n    }\n\n    @Test\n    fun `금액을 나누면 나눗셈 결과가 반환된다`() {\n        val a = Money.wons(6000L)\n        assertEquals(Money.wons(2000L), a.divide(3.0))\n    }\n\n    @Test\n    fun `작은 금액은 큰 금액보다 isLessThan이 true다`() {\n        val small = Money.wons(100L)\n        val big = Money.wons(200L)\n        assertTrue(small.isLessThan(big))\n    }\n\n    @Test\n    fun `큰 금액은 작은 금액보다 isLessThan이 false다`() {\n        val small = Money.wons(100L)\n        val big = Money.wons(200L)\n        assertFalse(big.isLessThan(small))\n    }\n\n    @Test\n    fun `같은 금액끼리 isLessThanOrEqual이 true다`() {\n        val a = Money.wons(500L)\n        val b = Money.wons(500L)\n        assertTrue(a.isLessThanOrEqual(b))\n    }\n\n    @Test\n    fun `큰 금액은 작은 금액보다 isGreaterThan이 true다`() {\n        val big = Money.wons(300L)\n        val small = Money.wons(100L)\n        assertTrue(big.isGreaterThan(small))\n    }\n\n    @Test\n    fun `같은 금액끼리 isGreaterThanOrEqual이 true다`() {\n        val a = Money.wons(1000L)\n        assertTrue(a.isGreaterThanOrEqual(a))\n    }\n\n    @Test\n    fun `같은 금액끼리 equals가 true다`() {\n        val a = Money.wons(1000L)\n        val b = Money.wons(1000L)\n        assertEquals(a, b)\n    }\n\n    @Test\n    fun `다른 금액끼리 equals가 false다`() {\n        val a = Money.wons(1000L)\n        val b = Money.wons(2000L)\n        assertNotEquals(a, b)\n    }\n\n    @Test\n    fun `같은 금액은 hashCode가 같다`() {\n        val a = Money.wons(1000L)\n        val b = Money.wons(1000L)\n        assertEquals(a.hashCode(), b.hashCode())\n    }\n\n    @Test\n    fun `sum은 컬렉션 원소들의 금액을 모두 더한다`() {\n        val prices = listOf(Money.wons(1000L), Money.wons(2000L), Money.wons(3000L))\n        val total = Money.sum(prices) { it }\n        assertEquals(Money.wons(6000L), total)\n    }\n\n    @Test\n    fun `빈 컬렉션의 sum은 ZERO다`() {\n        val total = Money.sum(emptyList<Money>()) { it }\n        assertEquals(Money.ZERO, total)\n    }\n\n    @Test\n    fun `getDiscountAmountByPercentage는 공급가에 퍼센트 비율을 적용한 할인금액을 반환한다`() {\n        val supply = Money.wons(10000L)\n        val discountMoney = Money.ZERO\n        val discountAmount = discountMoney.getDiscountAmountByPercentage(supply, 10L)\n        assertEquals(1000L, discountAmount)\n    }\n\n    @Test\n    fun `toString은 원 단위 문자열을 반환한다`() {\n        val money = Money.wons(5000L)\n        assertEquals(\"5000원\", money.toString())\n    }\n\n    @Test\n    fun `ZERO에서 ZERO를 더하면 ZERO다`() {\n        assertEquals(Money.ZERO, Money.ZERO.plus(Money.ZERO))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/kotlin/band/gosrock/domain/domains/cart/service/DoneOrderEventHandlerTest.kt",
    "content": "package band.gosrock.domain.domains.cart.service\n\nimport band.gosrock.domain.common.events.order.DoneOrderEvent\nimport band.gosrock.domain.domains.cart.adaptor.CartAdaptor\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.extension.ExtendWith\nimport org.mockito.BDDMockito.then\nimport org.mockito.InjectMocks\nimport org.mockito.Mock\nimport org.mockito.Mockito.times\nimport org.mockito.junit.jupiter.MockitoExtension\nimport java.lang.reflect.Constructor\n\n@ExtendWith(MockitoExtension::class)\nclass DoneOrderEventHandlerTest {\n\n    @Mock\n    lateinit var cartAdaptor: CartAdaptor\n\n    @InjectMocks\n    lateinit var handler: DoneOrderEventHandler\n\n    companion object {\n        private fun createDoneOrderEvent(userId: Long): DoneOrderEvent {\n            val constructor: Constructor<DoneOrderEvent> =\n                DoneOrderEvent::class.java.getDeclaredConstructor(\n                    String::class.java, Long::class.java,\n                    band.gosrock.domain.domains.order.domain.OrderMethod::class.java,\n                    String::class.java, Long::class.java\n                )\n            constructor.isAccessible = true\n            return constructor.newInstance(\n                \"test-order-uuid\", userId,\n                band.gosrock.domain.domains.order.domain.OrderMethod.APPROVAL, null, 100L\n            )\n        }\n    }\n\n    @Test\n    fun `주문 완료 이벤트 시 해당 유저의 장바구니를 삭제한다`() {\n        // given\n        val event = createDoneOrderEvent(1L)\n\n        // when\n        handler.handleDoneOrderEvent(event)\n\n        // then\n        then(cartAdaptor).should(times(1)).deleteByUserId(1L)\n    }\n\n    @Test\n    fun `주문 완료 이벤트가 여러 번 발생하면 그만큼 장바구니를 삭제한다`() {\n        // given\n        val event1 = createDoneOrderEvent(1L)\n        val event2 = createDoneOrderEvent(1L)\n\n        // when\n        handler.handleDoneOrderEvent(event1)\n        handler.handleDoneOrderEvent(event2)\n\n        // then\n        then(cartAdaptor).should(times(2)).deleteByUserId(1L)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/kotlin/band/gosrock/domain/domains/event/EventStatusTransitionTest.kt",
    "content": "package band.gosrock.domain.domains.event\n\nimport band.gosrock.domain.domains.event.domain.Event\nimport band.gosrock.domain.domains.event.domain.EventBasic\nimport band.gosrock.domain.domains.event.domain.EventStatus\nimport band.gosrock.domain.domains.event.exception.AlreadyCalculatingStatusException\nimport band.gosrock.domain.domains.event.exception.AlreadyCloseStatusException\nimport band.gosrock.domain.domains.event.exception.AlreadyDeletedStatusException\nimport band.gosrock.domain.domains.event.exception.AlreadyOpenStatusException\nimport band.gosrock.domain.domains.event.exception.AlreadyPreparingStatusException\nimport band.gosrock.domain.domains.event.exception.CannotDeleteByOpenEventException\nimport band.gosrock.domain.domains.event.exception.CannotModifyOpenEventException\nimport band.gosrock.domain.domains.event.exception.EventNotOpenException\nimport band.gosrock.domain.domains.event.exception.EventOpenTimeExpiredException\nimport band.gosrock.domain.domains.event.exception.InvalidEventStatusTransitionException\nimport band.gosrock.domain.domains.event.exception.EventTicketingTimeIsPassedException\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Assertions.assertFalse\nimport org.junit.jupiter.api.Assertions.assertNotNull\nimport org.junit.jupiter.api.Assertions.assertNull\nimport org.junit.jupiter.api.Assertions.assertThrows\nimport org.junit.jupiter.api.Assertions.assertTrue\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Test\nimport org.springframework.test.util.ReflectionTestUtils\nimport java.time.LocalDateTime\n\nclass EventStatusTransitionTest {\n\n    private lateinit var event: Event\n\n    @BeforeEach\n    fun setUp() {\n        event = Event()\n    }\n\n    // ---- hasEventBasic ----\n\n    @Test\n    fun `name과 startAt과 runTime이 모두 있으면 hasEventBasic이 true다`() {\n        val basic = EventBasic(\n            name = \"공연명\",\n            startAt = LocalDateTime.now().plusDays(1),\n            runTime = 90L,\n        )\n        event.updateEventBasic(basic)\n        assertTrue(event.hasEventBasic())\n    }\n\n    @Test\n    fun `eventBasic이 null이면 hasEventBasic이 false다`() {\n        event.updateEventBasic(null)\n        assertFalse(event.hasEventBasic())\n    }\n\n    @Test\n    fun `name이 없으면 hasEventBasic이 false다`() {\n        val basic = EventBasic(\n            startAt = LocalDateTime.now().plusDays(1),\n            runTime = 90L,\n        )\n        event.updateEventBasic(basic)\n        assertFalse(event.hasEventBasic())\n    }\n\n    // ---- isTimeBeforeStartAt ----\n\n    @Test\n    fun `startAt이 미래이면 isTimeBeforeStartAt이 true다`() {\n        val basic = EventBasic(\n            name = \"공연명\",\n            startAt = LocalDateTime.now().plusDays(1),\n            runTime = 90L,\n        )\n        event.updateEventBasic(basic)\n        assertTrue(event.isTimeBeforeStartAt())\n    }\n\n    @Test\n    fun `startAt이 과거이면 isTimeBeforeStartAt이 false다`() {\n        val basic = EventBasic(\n            name = \"공연명\",\n            startAt = LocalDateTime.now().minusMinutes(1),\n            runTime = 90L,\n        )\n        event.updateEventBasic(basic)\n        assertFalse(event.isTimeBeforeStartAt())\n    }\n\n    // ---- validateTicketingTime ----\n\n    @Test\n    fun `startAt이 과거이면 validateTicketingTime이 예외를 던진다`() {\n        val basic = EventBasic(\n            name = \"공연명\",\n            startAt = LocalDateTime.now().minusMinutes(1),\n            runTime = 90L,\n        )\n        event.updateEventBasic(basic)\n        assertThrows(EventTicketingTimeIsPassedException::class.java) {\n            event.validateTicketingTime()\n        }\n    }\n\n    @Test\n    fun `startAt이 미래이면 validateTicketingTime이 예외를 던지지 않는다`() {\n        val basic = EventBasic(\n            name = \"공연명\",\n            startAt = LocalDateTime.now().plusHours(1),\n            runTime = 90L,\n        )\n        event.updateEventBasic(basic)\n        event.validateTicketingTime() // no exception\n    }\n\n    // ---- status: PREPARING (default) ----\n\n    @Test\n    fun `신규 이벤트의 기본 상태는 PREPARING이다`() {\n        assertEquals(EventStatus.PREPARING, event.status)\n    }\n\n    @Test\n    fun `이미 PREPARING 상태에서 prepare를 호출하면 AlreadyPreparingStatusException이 발생한다`() {\n        assertThrows(AlreadyPreparingStatusException::class.java) {\n            event.prepare()\n        }\n    }\n\n    // ---- status: OPEN ----\n\n    @Test\n    fun `미래 startAt이 있는 이벤트는 open 상태로 변경된다`() {\n        val basic = EventBasic(\n            name = \"공연명\",\n            startAt = LocalDateTime.now().plusMinutes(10),\n            runTime = 90L,\n        )\n        event.updateEventBasic(basic)\n        event.open()\n        assertEquals(EventStatus.OPEN, event.status)\n    }\n\n    @Test\n    fun `이미 OPEN 상태에서 open을 호출하면 AlreadyOpenStatusException이 발생한다`() {\n        val basic = EventBasic(\n            name = \"공연명\",\n            startAt = LocalDateTime.now().plusMinutes(10),\n            runTime = 90L,\n        )\n        event.updateEventBasic(basic)\n        event.open()\n        assertThrows(AlreadyOpenStatusException::class.java) {\n            event.open()\n        }\n    }\n\n    @Test\n    fun `과거 startAt이 있는 이벤트는 open을 호출하면 EventOpenTimeExpiredException이 발생한다`() {\n        val basic = EventBasic(\n            name = \"공연명\",\n            startAt = LocalDateTime.now().minusMinutes(1),\n            runTime = 90L,\n        )\n        event.updateEventBasic(basic)\n        assertThrows(EventOpenTimeExpiredException::class.java) {\n            event.open()\n        }\n    }\n\n    // ---- status: CALCULATING ----\n\n    @Test\n    fun `OPEN 이벤트는 CALCULATING으로 변경된다`() {\n        ReflectionTestUtils.setField(event, \"status\", EventStatus.OPEN)\n        event.calculate()\n        assertEquals(EventStatus.CALCULATING, event.status)\n    }\n\n    @Test\n    fun `PREPARING에서 CALCULATING으로 직접 전이는 불가하다`() {\n        assertThrows(InvalidEventStatusTransitionException::class.java) {\n            event.calculate()\n        }\n    }\n\n    @Test\n    fun `이미 CALCULATING 상태에서 calculate를 호출하면 AlreadyCalculatingStatusException이 발생한다`() {\n        ReflectionTestUtils.setField(event, \"status\", EventStatus.OPEN)\n        event.calculate()\n        assertThrows(AlreadyCalculatingStatusException::class.java) {\n            event.calculate()\n        }\n    }\n\n    // ---- status: CLOSED ----\n\n    @Test\n    fun `CALCULATING 이벤트는 CLOSED로 변경된다`() {\n        ReflectionTestUtils.setField(event, \"status\", EventStatus.CALCULATING)\n        event.close()\n        assertEquals(EventStatus.CLOSED, event.status)\n    }\n\n    @Test\n    fun `OPEN에서 CLOSED로 직접 전이는 불가하다`() {\n        ReflectionTestUtils.setField(event, \"status\", EventStatus.OPEN)\n        assertThrows(InvalidEventStatusTransitionException::class.java) {\n            event.close()\n        }\n    }\n\n    @Test\n    fun `이미 CLOSED 상태에서 close를 호출하면 AlreadyCloseStatusException이 발생한다`() {\n        ReflectionTestUtils.setField(event, \"status\", EventStatus.CALCULATING)\n        event.close()\n        assertThrows(AlreadyCloseStatusException::class.java) {\n            event.close()\n        }\n    }\n\n    // ---- status: DELETED ----\n\n    @Test\n    fun `PREPARING 이벤트는 소프트 삭제된다`() {\n        event.deleteSoft()\n        assertEquals(EventStatus.DELETED, event.status)\n    }\n\n    @Test\n    fun `OPEN 이벤트는 deleteSoft를 호출하면 CannotDeleteByOpenEventException이 발생한다`() {\n        ReflectionTestUtils.setField(event, \"status\", EventStatus.OPEN)\n        assertThrows(CannotDeleteByOpenEventException::class.java) {\n            event.deleteSoft()\n        }\n    }\n\n    @Test\n    fun `이미 DELETED 상태에서 deleteSoft를 호출하면 AlreadyDeletedStatusException이 발생한다`() {\n        event.deleteSoft()\n        assertThrows(AlreadyDeletedStatusException::class.java) {\n            event.deleteSoft()\n        }\n    }\n\n    // ---- validateOpenStatus / validateNotOpenStatus ----\n\n    @Test\n    fun `OPEN 상태에서 validateOpenStatus를 호출하면 CannotModifyOpenEventException이 발생한다`() {\n        ReflectionTestUtils.setField(event, \"status\", EventStatus.OPEN)\n        assertThrows(CannotModifyOpenEventException::class.java) {\n            event.validateOpenStatus()\n        }\n    }\n\n    @Test\n    fun `PREPARING 상태에서 validateNotOpenStatus를 호출하면 EventNotOpenException이 발생한다`() {\n        assertThrows(EventNotOpenException::class.java) {\n            event.validateNotOpenStatus()\n        }\n    }\n\n    @Test\n    fun `OPEN 상태에서 validateNotOpenStatus는 예외를 던지지 않는다`() {\n        ReflectionTestUtils.setField(event, \"status\", EventStatus.OPEN)\n        event.validateNotOpenStatus() // no exception\n    }\n\n    // ---- getEventName ----\n\n    @Test\n    fun `eventBasic이 있으면 getEventName이 이름을 반환한다`() {\n        val basic = EventBasic(\n            name = \"두둥 공연\",\n            startAt = LocalDateTime.now().plusDays(1),\n            runTime = 60L,\n        )\n        event.updateEventBasic(basic)\n        assertEquals(\"두둥 공연\", event.getEventName())\n    }\n\n    @Test\n    fun `eventBasic이 null이면 getEventName이 null을 반환한다`() {\n        assertNull(event.getEventName())\n    }\n\n    // ---- isPreparing / isClosed ----\n\n    @Test\n    fun `PREPARING 상태이면 isPreparing이 true다`() {\n        assertTrue(event.isPreparing())\n    }\n\n    @Test\n    fun `CLOSED 상태이면 isClosed가 true다`() {\n        ReflectionTestUtils.setField(event, \"status\", EventStatus.CLOSED)\n        assertTrue(event.isClosed())\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/kotlin/band/gosrock/domain/domains/host/HostTransferMasterTest.kt",
    "content": "package band.gosrock.domain.domains.host\n\nimport band.gosrock.domain.domains.host.domain.Host\nimport band.gosrock.domain.domains.host.domain.HostRole\nimport band.gosrock.domain.domains.host.domain.HostUser\nimport band.gosrock.domain.domains.host.exception.ForbiddenHostException\nimport band.gosrock.domain.domains.host.exception.NotMasterHostException\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.assertThrows\nimport org.springframework.test.util.ReflectionTestUtils\n\nclass HostTransferMasterTest {\n\n    private lateinit var host: Host\n    private val masterUserId = 1L\n    private val memberUserId = 2L\n\n    @BeforeEach\n    fun setUp() {\n        host = Host(masterUserId = masterUserId)\n        ReflectionTestUtils.setField(host, \"id\", 100L)\n\n        val masterHostUser = HostUser(host = host, userId = masterUserId, role = HostRole.MASTER)\n        ReflectionTestUtils.setField(masterHostUser, \"active\", true)\n\n        val memberHostUser = HostUser(host = host, userId = memberUserId, role = HostRole.MANAGER)\n        ReflectionTestUtils.setField(memberHostUser, \"active\", true)\n\n        host.hostUsers.addAll(setOf(masterHostUser, memberHostUser))\n    }\n\n    @Test\n    fun `마스터가 활성 멤버에게 양도하면 성공한다`() {\n        host.transferMaster(masterUserId, memberUserId)\n\n        assertEquals(memberUserId, host.masterUserId)\n        assertEquals(HostRole.MANAGER, host.getHostUserByUserId(masterUserId).role)\n        assertEquals(HostRole.MASTER, host.getHostUserByUserId(memberUserId).role)\n    }\n\n    @Test\n    fun `마스터가 아닌 유저가 양도를 시도하면 예외가 발생한다`() {\n        assertThrows<NotMasterHostException> {\n            host.transferMaster(memberUserId, masterUserId)\n        }\n    }\n\n    @Test\n    fun `호스트에 속하지 않은 유저에게 양도하면 예외가 발생한다`() {\n        val nonMemberUserId = 999L\n        assertThrows<ForbiddenHostException> {\n            host.transferMaster(masterUserId, nonMemberUserId)\n        }\n    }\n\n    @Test\n    fun `forceTransferMaster는 권한 검증 없이 양도한다`() {\n        host.forceTransferMaster(memberUserId)\n\n        assertEquals(memberUserId, host.masterUserId)\n        assertEquals(HostRole.MANAGER, host.getHostUserByUserId(masterUserId).role)\n        assertEquals(HostRole.MASTER, host.getHostUserByUserId(memberUserId).role)\n    }\n\n    @Test\n    fun `forceTransferMaster로 비멤버에게 양도하면 예외가 발생한다`() {\n        val nonMemberUserId = 999L\n        assertThrows<ForbiddenHostException> {\n            host.forceTransferMaster(nonMemberUserId)\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/kotlin/band/gosrock/domain/domains/issuedTicket/IssuedTicketStatusTransitionTest.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket\n\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicket\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketItemInfoVo\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketOptionAnswer\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketStatus\nimport band.gosrock.domain.domains.issuedTicket.domain.IssuedTicketUserInfoVo\nimport band.gosrock.domain.domains.issuedTicket.exception.CanNotCancelEntranceException\nimport band.gosrock.domain.domains.issuedTicket.exception.CanNotCancelException\nimport band.gosrock.domain.domains.issuedTicket.exception.CanNotEntranceException\nimport band.gosrock.domain.domains.issuedTicket.exception.IssuedTicketAlreadyEntranceException\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Assertions.assertThrows\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.extension.ExtendWith\nimport org.mockito.Mock\nimport org.mockito.Mockito.`when`\nimport org.mockito.junit.jupiter.MockitoExtension\nimport org.springframework.test.util.ReflectionTestUtils\n\n@ExtendWith(MockitoExtension::class)\nclass IssuedTicketStatusTransitionTest {\n\n    @Mock\n    private lateinit var userInfo: IssuedTicketUserInfoVo\n\n    @Mock\n    private lateinit var itemInfo: IssuedTicketItemInfoVo\n\n    private lateinit var issuedTicket: IssuedTicket\n\n    @BeforeEach\n    fun setUp() {\n        issuedTicket = IssuedTicket(\n            eventId = 1L,\n            userInfo = userInfo,\n            itemInfo = itemInfo,\n            orderUuid = \"test-uuid\",\n            orderLineId = 10L,\n            issuedTicketStatus = IssuedTicketStatus.ENTRANCE_INCOMPLETE,\n            initialOptionAnswers = emptyList(),\n        )\n    }\n\n    // ---- 기본 상태 ----\n\n    @Test\n    fun `신규 발급 티켓의 기본 상태는 ENTRANCE_INCOMPLETE다`() {\n        assertEquals(IssuedTicketStatus.ENTRANCE_INCOMPLETE, issuedTicket.issuedTicketStatus)\n    }\n\n    // ---- cancel ----\n\n    @Test\n    fun `ENTRANCE_INCOMPLETE 상태의 티켓은 취소할 수 있다`() {\n        issuedTicket.cancel()\n        assertEquals(IssuedTicketStatus.CANCELED, issuedTicket.issuedTicketStatus)\n    }\n\n    @Test\n    fun `이미 입장 완료된 티켓은 취소 시 CanNotCancelException이 발생한다`() {\n        ReflectionTestUtils.setField(issuedTicket, \"issuedTicketStatus\", IssuedTicketStatus.ENTRANCE_COMPLETED)\n        assertThrows(CanNotCancelException::class.java) {\n            issuedTicket.cancel()\n        }\n    }\n\n    @Test\n    fun `이미 취소된 티켓은 다시 취소 시 CanNotCancelException이 발생한다`() {\n        issuedTicket.cancel()\n        assertThrows(CanNotCancelException::class.java) {\n            issuedTicket.cancel()\n        }\n    }\n\n    // ---- entrance ----\n\n    @Test\n    fun `ENTRANCE_INCOMPLETE 상태의 티켓은 입장 처리된다`() {\n        issuedTicket.entrance()\n        assertEquals(IssuedTicketStatus.ENTRANCE_COMPLETED, issuedTicket.issuedTicketStatus)\n    }\n\n    @Test\n    fun `이미 입장 완료된 티켓은 재입장 시 IssuedTicketAlreadyEntranceException이 발생한다`() {\n        issuedTicket.entrance()\n        assertThrows(IssuedTicketAlreadyEntranceException::class.java) {\n            issuedTicket.entrance()\n        }\n    }\n\n    @Test\n    fun `취소된 티켓은 입장 시 CanNotEntranceException이 발생한다`() {\n        issuedTicket.cancel()\n        assertThrows(CanNotEntranceException::class.java) {\n            issuedTicket.entrance()\n        }\n    }\n\n    // ---- entranceCancel ----\n\n    @Test\n    fun `ENTRANCE_COMPLETED 상태의 티켓은 입장 취소 처리된다`() {\n        issuedTicket.entrance()\n        issuedTicket.entranceCancel()\n        assertEquals(IssuedTicketStatus.ENTRANCE_INCOMPLETE, issuedTicket.issuedTicketStatus)\n    }\n\n    @Test\n    fun `ENTRANCE_INCOMPLETE 상태에서 입장 취소 시 CanNotCancelEntranceException이 발생한다`() {\n        assertThrows(CanNotCancelEntranceException::class.java) {\n            issuedTicket.entranceCancel()\n        }\n    }\n\n    @Test\n    fun `CANCELED 상태에서 입장 취소 시 CanNotCancelEntranceException이 발생한다`() {\n        issuedTicket.cancel()\n        assertThrows(CanNotCancelEntranceException::class.java) {\n            issuedTicket.entranceCancel()\n        }\n    }\n\n    // ---- sumOptionPrice ----\n\n    @Test\n    fun `옵션 답변이 없으면 sumOptionPrice는 ZERO다`() {\n        assertEquals(Money.ZERO, issuedTicket.sumOptionPrice())\n    }\n\n    @Test\n    fun `옵션 답변이 있으면 sumOptionPrice는 추가금액 합계를 반환한다`() {\n        val answer1 = IssuedTicketOptionAnswer(optionId = 1L, additionalPrice = Money.wons(1000L), answer = \"답변1\")\n        val answer2 = IssuedTicketOptionAnswer(optionId = 2L, additionalPrice = Money.wons(2000L), answer = \"답변2\")\n        issuedTicket.addOptionAnswers(listOf(answer1, answer2))\n        assertEquals(Money.wons(3000L), issuedTicket.sumOptionPrice())\n    }\n\n    // ---- IssuedTicketStatus 열거형 메서드 ----\n\n    @Test\n    fun `CANCELED 상태의 isCanceled는 true다`() {\n        assert(IssuedTicketStatus.CANCELED.isCanceled())\n    }\n\n    @Test\n    fun `ENTRANCE_INCOMPLETE 상태의 isBeforeEntrance는 true다`() {\n        assert(IssuedTicketStatus.ENTRANCE_INCOMPLETE.isBeforeEntrance())\n    }\n\n    @Test\n    fun `ENTRANCE_COMPLETED 상태의 isAfterEntrance는 true다`() {\n        assert(IssuedTicketStatus.ENTRANCE_COMPLETED.isAfterEntrance())\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/kotlin/band/gosrock/domain/domains/issuedTicket/adaptor/IssuedTicketAdaptorTest.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.adaptor\n\nimport org.junit.jupiter.api.extension.ExtendWith\nimport org.mockito.junit.jupiter.MockitoExtension\n\n@ExtendWith(MockitoExtension::class)\nclass IssuedTicketAdaptorTest\n"
  },
  {
    "path": "DuDoong-Domain/src/test/kotlin/band/gosrock/domain/domains/issuedTicket/domain/IssuedTicketItemInfoVoTest.kt",
    "content": "package band.gosrock.domain.domains.issuedTicket.domain\n\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.ticket_item.domain.TicketItem\nimport band.gosrock.domain.domains.ticket_item.domain.TicketPayType\nimport band.gosrock.domain.domains.ticket_item.domain.TicketType\nimport org.junit.jupiter.api.Assertions.assertAll\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.extension.ExtendWith\nimport org.mockito.junit.jupiter.MockitoExtension\nimport java.time.LocalDateTime\n\n@ExtendWith(MockitoExtension::class)\nclass IssuedTicketItemInfoVoTest {\n\n    var itemInfoVo: IssuedTicketItemInfoVo? = null\n\n    private val ticketType: TicketType = TicketType.FIRST_COME_FIRST_SERVED\n\n    private val payType: TicketPayType = TicketPayType.DUDOONG_TICKET\n\n    private val w3000: Money = Money.wons(3000L)\n\n    private val startAt: LocalDateTime = LocalDateTime.now()\n\n    private val endAt: LocalDateTime = startAt.plusDays(1L)\n\n    @BeforeEach\n    fun setUp() {\n        itemInfoVo = IssuedTicketItemInfoVo(1L, ticketType, payType, \"testTicket\", w3000)\n    }\n\n    @Test\n    fun 티켓_아이템_정보를_발급티켓_아이템_인포로_정상적으로_변환_테스트() {\n        // given\n        val newTicketItem = TicketItem(\n            TicketPayType.DUDOONG_TICKET, \"testTicket\", \"test\", w3000, 1L, 1L, 1L, ticketType,\n            \"test\", \"test\", \"test\", true, true, startAt, endAt, 1L\n        )\n\n        // when\n        val itemInfoVoForTest = IssuedTicketItemInfoVo.from(newTicketItem)\n\n        // then\n        assertAll(\n            { assertEquals(itemInfoVo!!.price, itemInfoVoForTest.price) },\n            { assertEquals(itemInfoVo!!.ticketName, itemInfoVoForTest.ticketName) },\n            { assertEquals(itemInfoVo!!.ticketType, itemInfoVoForTest.ticketType) },\n            { assertEquals(itemInfoVo!!.payType, itemInfoVoForTest.payType) }\n        )\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/kotlin/band/gosrock/domain/domains/order/OrderPaymentCalculationTest.kt",
    "content": "package band.gosrock.domain.domains.order\n\nimport band.gosrock.domain.common.vo.Money\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderCouponVo\nimport band.gosrock.domain.domains.order.domain.OrderLineItem\nimport band.gosrock.domain.domains.order.domain.OrderMethod\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Assertions.assertFalse\nimport org.junit.jupiter.api.Assertions.assertTrue\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.extension.ExtendWith\nimport org.mockito.BDDMockito.given\nimport org.mockito.Mock\nimport org.mockito.junit.jupiter.MockitoExtension\n\n@ExtendWith(MockitoExtension::class)\nclass OrderPaymentCalculationTest {\n\n    @Mock\n    private lateinit var lineItem1: OrderLineItem\n\n    @Mock\n    private lateinit var lineItem2: OrderLineItem\n\n    @Mock\n    private lateinit var orderCouponVo: OrderCouponVo\n\n    private lateinit var noCouponOrder: Order\n    private lateinit var couponOrder: Order\n\n    @BeforeEach\n    fun setUp() {\n        noCouponOrder = Order.forTest(\n            userId = 1L,\n            orderName = \"쿠폰없는주문\",\n            orderLineItems = listOf(lineItem1, lineItem2),\n            orderStatus = OrderStatus.PENDING_PAYMENT,\n            orderMethod = OrderMethod.PAYMENT,\n            eventId = 100L,\n        )\n\n        couponOrder = Order.forTest(\n            userId = 1L,\n            orderName = \"쿠폰있는주문\",\n            orderLineItems = listOf(lineItem1, lineItem2),\n            orderStatus = OrderStatus.PENDING_PAYMENT,\n            orderMethod = OrderMethod.PAYMENT,\n            eventId = 100L,\n        )\n        couponOrder.attachCoupon(orderCouponVo)\n    }\n\n    // ---- 할인 없는 주문 ----\n\n    @Test\n    fun `쿠폰 없는 주문의 총 할인금액은 0원이다`() {\n        assertEquals(Money.ZERO, noCouponOrder.getTotalDiscountPrice())\n    }\n\n    @Test\n    fun `총 공급가액은 라인아이템 합계다`() {\n        given(lineItem1.getTotalOrderLinePrice()).willReturn(Money.wons(3000L))\n        given(lineItem2.getTotalOrderLinePrice()).willReturn(Money.wons(4000L))\n\n        assertEquals(Money.wons(7000L), noCouponOrder.getTotalSupplyPrice())\n    }\n\n    @Test\n    fun `쿠폰 없을때 총 결제금액은 총 공급가액과 같다`() {\n        given(lineItem1.getTotalOrderLinePrice()).willReturn(Money.wons(3000L))\n        given(lineItem2.getTotalOrderLinePrice()).willReturn(Money.wons(4000L))\n\n        assertEquals(Money.wons(7000L), noCouponOrder.getTotalPaymentPrice())\n    }\n\n    // ---- 쿠폰 있는 주문 ----\n\n    @Test\n    fun `쿠폰 할인 후 결제금액은 공급가액에서 할인액을 뺀 금액이다`() {\n        given(lineItem1.getTotalOrderLinePrice()).willReturn(Money.wons(3000L))\n        given(lineItem2.getTotalOrderLinePrice()).willReturn(Money.wons(4000L))\n        given(orderCouponVo.discountAmount).willReturn(Money.wons(2000L))\n\n        assertEquals(Money.wons(5000L), couponOrder.getTotalPaymentPrice())\n    }\n\n    @Test\n    fun `쿠폰으로 전액 할인되면 결제금액이 0원이다`() {\n        given(lineItem1.getTotalOrderLinePrice()).willReturn(Money.wons(3000L))\n        given(lineItem2.getTotalOrderLinePrice()).willReturn(Money.wons(4000L))\n        given(orderCouponVo.discountAmount).willReturn(Money.wons(7000L))\n\n        assertEquals(Money.ZERO, couponOrder.getTotalPaymentPrice())\n    }\n\n    // ---- isNeedPaid ----\n\n    @Test\n    fun `결제금액이 0원보다 크고 PAYMENT 방식이면 isNeedPaid가 true다`() {\n        given(lineItem1.getTotalOrderLinePrice()).willReturn(Money.wons(1000L))\n        given(lineItem2.getTotalOrderLinePrice()).willReturn(Money.wons(1000L))\n\n        assertTrue(noCouponOrder.isNeedPaid())\n    }\n\n    @Test\n    fun `쿠폰으로 전액 할인되면 isNeedPaid가 false다`() {\n        given(lineItem1.getTotalOrderLinePrice()).willReturn(Money.wons(3000L))\n        given(lineItem2.getTotalOrderLinePrice()).willReturn(Money.wons(4000L))\n        given(orderCouponVo.discountAmount).willReturn(Money.wons(7000L))\n\n        assertFalse(couponOrder.isNeedPaid())\n    }\n\n    // ---- hasCoupon ----\n\n    @Test\n    fun `쿠폰 없는 주문의 hasCoupon은 false다`() {\n        assertFalse(noCouponOrder.hasCoupon())\n    }\n\n    @Test\n    fun `쿠폰 있는 주문의 hasCoupon은 true다`() {\n        given(orderCouponVo.isDefault()).willReturn(false)\n        assertTrue(couponOrder.hasCoupon())\n    }\n\n    // ---- getTotalQuantity ----\n\n    @Test\n    fun `getTotalQuantity는 모든 라인아이템의 수량 합계를 반환한다`() {\n        given(lineItem1.quantity).willReturn(2L)\n        given(lineItem2.quantity).willReturn(3L)\n\n        assertEquals(5L, noCouponOrder.getTotalQuantity())\n    }\n\n    // ---- fail ----\n\n    @Test\n    fun `fail 호출 시 orderStatus가 FAILED로 변경된다`() {\n        noCouponOrder.fail()\n        assertEquals(OrderStatus.FAILED, noCouponOrder.orderStatus)\n    }\n\n    // ---- isDudoongTicketOrder ----\n\n    @Test\n    fun `결제금액이 0원이고 APPROVAL 방식이면 isDudoongTicketOrder가 false다`() {\n        val approvalOrder = Order.forTest(\n            userId = 1L,\n            orderName = \"승인주문\",\n            orderLineItems = listOf(lineItem1, lineItem2),\n            orderStatus = OrderStatus.PENDING_APPROVE,\n            orderMethod = OrderMethod.APPROVAL,\n            eventId = 100L,\n        )\n\n        given(lineItem1.getTotalOrderLinePrice()).willReturn(Money.ZERO)\n        given(lineItem2.getTotalOrderLinePrice()).willReturn(Money.ZERO)\n\n        assertFalse(approvalOrder.isDudoongTicketOrder())\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/kotlin/band/gosrock/domain/domains/order/OrderRefundTest.kt",
    "content": "package band.gosrock.domain.domains.order\n\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderMethod\nimport band.gosrock.domain.domains.order.domain.OrderStatus\nimport band.gosrock.domain.domains.order.domain.RefundStatus\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Assertions.assertNotNull\nimport org.junit.jupiter.api.Assertions.assertNull\nimport org.junit.jupiter.api.Test\n\nclass OrderRefundTest {\n\n    @Test\n    fun `초기 refundStatus는 NONE이다`() {\n        val order = createTestOrder()\n        assertEquals(RefundStatus.NONE, order.refundStatus)\n        assertNull(order.refundStatusChangedAt)\n    }\n\n    @Test\n    fun `forTest로 cancel 상태와 REFUND_REQUESTED를 설정할 수 있다`() {\n        val order = Order.forTest(\n            userId = 1L,\n            orderName = \"테스트주문\",\n            orderStatus = OrderStatus.CANCELED,\n            orderMethod = OrderMethod.PAYMENT,\n            eventId = 100L,\n            cancelReason = \"단순 변심\",\n            refundStatus = RefundStatus.REFUND_REQUESTED,\n        )\n\n        assertEquals(OrderStatus.CANCELED, order.orderStatus)\n        assertEquals(\"단순 변심\", order.cancelReason)\n        assertEquals(RefundStatus.REFUND_REQUESTED, order.refundStatus)\n    }\n\n    @Test\n    fun `forTest로 refund 상태와 reason을 설정할 수 있다`() {\n        val order = Order.forTest(\n            userId = 1L,\n            orderName = \"테스트주문\",\n            orderStatus = OrderStatus.REFUND,\n            orderMethod = OrderMethod.PAYMENT,\n            eventId = 100L,\n            cancelReason = \"환불 요청합니다\",\n            refundStatus = RefundStatus.REFUND_REQUESTED,\n        )\n\n        assertEquals(OrderStatus.REFUND, order.orderStatus)\n        assertEquals(\"환불 요청합니다\", order.cancelReason)\n        assertEquals(RefundStatus.REFUND_REQUESTED, order.refundStatus)\n    }\n\n    @Test\n    fun `completeRefund 호출 시 refundStatus가 REFUND_COMPLETED로 변경된다`() {\n        val order = Order.forTest(\n            userId = 1L,\n            orderName = \"테스트주문\",\n            orderStatus = OrderStatus.CANCELED,\n            orderMethod = OrderMethod.PAYMENT,\n            eventId = 100L,\n            cancelReason = \"단순 변심\",\n            refundStatus = RefundStatus.REFUND_REQUESTED,\n        )\n        order.completeRefund()\n\n        assertEquals(RefundStatus.REFUND_COMPLETED, order.refundStatus)\n        assertNotNull(order.refundStatusChangedAt)\n    }\n\n    @Test\n    fun `fail에 reason을 전달하면 failReason이 설정된다`() {\n        val order = createTestOrder()\n        order.fail(\"결제 실패\")\n\n        assertEquals(OrderStatus.FAILED, order.orderStatus)\n        assertEquals(\"결제 실패\", order.failReason)\n    }\n\n    @Test\n    fun `fail에 reason 없이 호출하면 failReason이 null이다`() {\n        val order = createTestOrder()\n        order.fail()\n\n        assertEquals(OrderStatus.FAILED, order.orderStatus)\n        assertNull(order.failReason)\n    }\n\n    @Test\n    fun `fail 시 501자 reason이 500자로 truncate된다`() {\n        val order = createTestOrder()\n        val longReason = \"가\".repeat(501)\n        order.fail(longReason)\n\n        assertEquals(500, order.failReason!!.length)\n    }\n\n    private fun createTestOrder(): Order = Order.forTest(\n        userId = 1L,\n        orderName = \"테스트주문\",\n        orderStatus = OrderStatus.CONFIRM,\n        orderMethod = OrderMethod.PAYMENT,\n        eventId = 100L,\n    )\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/kotlin/band/gosrock/domain/domains/order/service/handler/ConfirmOrderFailHandlerTest.kt",
    "content": "package band.gosrock.domain.domains.order.service.handler\n\nimport band.gosrock.domain.common.events.order.DoneOrderEvent\nimport band.gosrock.domain.domains.coupon.service.RecoveryCouponService\nimport band.gosrock.domain.domains.issuedTicket.service.IssuedTicketDomainService\nimport band.gosrock.domain.domains.order.adaptor.OrderAdaptor\nimport band.gosrock.domain.domains.order.domain.Order\nimport band.gosrock.domain.domains.order.domain.OrderMethod\nimport band.gosrock.domain.domains.order.service.WithdrawPaymentService\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.extension.ExtendWith\nimport org.mockito.ArgumentMatchers.anyLong\nimport org.mockito.ArgumentMatchers.anyString\nimport org.mockito.ArgumentMatchers.nullable\nimport org.mockito.BDDMockito.given\nimport org.mockito.BDDMockito.then\nimport org.mockito.InjectMocks\nimport org.mockito.Mock\nimport org.mockito.Mockito.never\nimport org.mockito.Mockito.times\nimport org.mockito.junit.jupiter.MockitoExtension\nimport java.lang.reflect.Constructor\n\n@ExtendWith(MockitoExtension::class)\nclass ConfirmOrderFailHandlerTest {\n\n    @Mock\n    lateinit var cancelPaymentService: WithdrawPaymentService\n\n    @Mock\n    lateinit var issuedTicketDomainService: IssuedTicketDomainService\n\n    @Mock\n    lateinit var recoveryCouponService: RecoveryCouponService\n\n    @Mock\n    lateinit var orderAdaptor: OrderAdaptor\n\n    @InjectMocks\n    lateinit var handler: ConfirmOrderFailHandler\n\n    @Mock\n    lateinit var order: Order\n\n    companion object {\n        private fun createDoneOrderEvent(userId: Long = 1L, paymentKey: String? = null): DoneOrderEvent {\n            val constructor: Constructor<DoneOrderEvent> =\n                DoneOrderEvent::class.java.getDeclaredConstructor(\n                    String::class.java, Long::class.java,\n                    OrderMethod::class.java,\n                    String::class.java, Long::class.java\n                )\n            constructor.isAccessible = true\n            return constructor.newInstance(\"test-order-uuid\", userId, OrderMethod.APPROVAL, paymentKey, 100L)\n        }\n    }\n\n    @Test\n    fun `주문 실패 이벤트 시 주문을 실패 상태로 변경한다`() {\n        // given\n        val event = createDoneOrderEvent()\n        given(orderAdaptor.findByOrderUuid(\"test-order-uuid\")).willReturn(order)\n        given(order.hasCoupon()).willReturn(false)\n        given(order.isNeedPaid()).willReturn(false)\n\n        // when\n        handler.handleDoneOrderFailEvent(event)\n\n        // then\n        then(order).should(times(1)).fail(nullable(String::class.java))\n    }\n\n    @Test\n    fun `주문 실패 이벤트 시 발급 티켓 회수를 실행한다`() {\n        // given\n        val event = createDoneOrderEvent()\n        given(orderAdaptor.findByOrderUuid(\"test-order-uuid\")).willReturn(order)\n        given(order.hasCoupon()).willReturn(false)\n        given(order.isNeedPaid()).willReturn(false)\n\n        // when\n        handler.handleDoneOrderFailEvent(event)\n\n        // then\n        then(issuedTicketDomainService).should(times(1))\n            .doneOrderEventAfterRollBackWithdrawIssuedTickets(100L, \"test-order-uuid\")\n    }\n\n    @Test\n    fun `쿠폰이 없는 주문 실패 시 쿠폰 복구를 실행하지 않는다`() {\n        // given\n        val event = createDoneOrderEvent()\n        given(orderAdaptor.findByOrderUuid(\"test-order-uuid\")).willReturn(order)\n        given(order.hasCoupon()).willReturn(false)\n        given(order.isNeedPaid()).willReturn(false)\n\n        // when\n        handler.handleDoneOrderFailEvent(event)\n\n        // then\n        then(recoveryCouponService).should(never()).execute(anyLong(), anyLong())\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/kotlin/band/gosrock/domain/domains/user/domain/ProfileTest.kt",
    "content": "package band.gosrock.domain.domains.user.domain\n\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.DisplayName\nimport org.junit.jupiter.api.Nested\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.assertThrows\n\n@DisplayName(\"Profile\")\nclass ProfileTest {\n\n    @Nested\n    @DisplayName(\"changeName\")\n    inner class ChangeNameTest {\n\n        @Test\n        @DisplayName(\"정상적인 이름으로 변경하면 성공한다\")\n        fun successWithValidName() {\n            val profile = Profile(name = \"기존이름\")\n            profile.changeName(\"새이름입니다\")\n            assertEquals(\"새이름입니다\", profile.name)\n        }\n\n        @Test\n        @DisplayName(\"2자 이름으로 변경하면 성공한다\")\n        fun successWithMinLength() {\n            val profile = Profile(name = \"기존이름\")\n            profile.changeName(\"두글\")\n            assertEquals(\"두글\", profile.name)\n        }\n\n        @Test\n        @DisplayName(\"7자 이름으로 변경하면 성공한다\")\n        fun successWithMaxLength() {\n            val profile = Profile(name = \"기존이름\")\n            val name7 = \"가\".repeat(7)\n            profile.changeName(name7)\n            assertEquals(name7, profile.name)\n        }\n\n        @Test\n        @DisplayName(\"빈 문자열이면 예외가 발생한다\")\n        fun failWithBlankName() {\n            val profile = Profile(name = \"기존이름\")\n            assertThrows<IllegalArgumentException> {\n                profile.changeName(\"\")\n            }\n        }\n\n        @Test\n        @DisplayName(\"공백만 있는 문자열이면 예외가 발생한다\")\n        fun failWithWhitespaceName() {\n            val profile = Profile(name = \"기존이름\")\n            assertThrows<IllegalArgumentException> {\n                profile.changeName(\"   \")\n            }\n        }\n\n        @Test\n        @DisplayName(\"1자 이름이면 예외가 발생한다\")\n        fun failWithTooShortName() {\n            val profile = Profile(name = \"기존이름\")\n            assertThrows<IllegalArgumentException> {\n                profile.changeName(\"가\")\n            }\n        }\n\n        @Test\n        @DisplayName(\"16자 이름이면 예외가 발생한다\")\n        fun failWithTooLongName() {\n            val profile = Profile(name = \"기존이름\")\n            val name16 = \"가\".repeat(16)\n            assertThrows<IllegalArgumentException> {\n                profile.changeName(name16)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Domain/src/test/resources/application-test.yml",
    "content": "ableDomainEvent : false"
  },
  {
    "path": "DuDoong-Domain/src/test/resources/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n  <include resource=\"org/springframework/boot/logging/logback/base.xml\" />\n  <logger name=\"org.springframework\" level=\"INFO\"/>\n  <include resource=\"band.gosrock\"/>\n  <logger name=\"band.gosrock\" level=\"DEBUG\"/>\n</configuration>"
  },
  {
    "path": "DuDoong-Domain/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker",
    "content": "mock-maker-inline\n"
  },
  {
    "path": "DuDoong-Infrastructure/build.gradle.kts",
    "content": "tasks.bootJar { enabled = false }\ntasks.jar { enabled = true }\n\ndependencies {\n    api(\"com.slack.api:slack-api-client:1.27.2\")\n    api(\"io.github.openfeign:feign-httpclient:12.1\")\n    api(\"org.springframework.cloud:spring-cloud-starter-openfeign:4.1.0\")\n    api(project(\":DuDoong-Common\"))\n    api(\"org.springframework.boot:spring-boot-starter-data-redis\")\n    api(\"org.redisson:redisson:3.25.2\")\n    api(\"com.fasterxml.jackson.datatype:jackson-datatype-jdk8\")\n    api(\"com.fasterxml.jackson.datatype:jackson-datatype-jsr310\")\n    api(\"com.amazonaws:aws-java-sdk-s3control:1.12.372\")\n    api(\"io.github.openfeign:feign-jackson:12.1\")\n    api(\"com.bucket4j:bucket4j-core:8.1.1\")\n    api(\"com.bucket4j:bucket4j-jcache:8.1.1\")\n\n    // for email\n    api(\"org.springframework.boot:spring-boot-starter-thymeleaf\")\n    api(\"nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect\")\n    api(\"software.amazon.awssdk:ses:2.19.29\")\n\n    api(\"com.googlecode.libphonenumber:libphonenumber:8.13.5\")\n    api(\"org.xhtmlrenderer:flying-saucer-pdf:9.1.20\")\n    api(\"com.sun.mail:jakarta.mail:2.0.1\")\n\n    testImplementation(\"org.springframework.boot:spring-boot-starter-thymeleaf\")\n    testImplementation(\"org.springframework.cloud:spring-cloud-starter-contract-stub-runner:4.1.0\")\n    testImplementation(\"org.springframework.cloud:spring-cloud-contract-wiremock:4.1.0\")\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/DuDoongInfraApplication.kt",
    "content": "package band.gosrock.infrastructure\n\nimport org.slf4j.LoggerFactory\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.boot.context.event.ApplicationReadyEvent\nimport org.springframework.context.ApplicationListener\nimport org.springframework.core.env.Environment\n\n// 테스팅 용도 어플리케이션입니다.\n@SpringBootApplication\nclass DuDoongInfraApplication(\n    private val environment: Environment,\n) : ApplicationListener<ApplicationReadyEvent> {\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    override fun onApplicationEvent(event: ApplicationReadyEvent) {\n        log.info(\"applicationReady status\" + environment.activeProfiles.contentToString())\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/alilmTalk/NcpHelper.kt",
    "content": "package band.gosrock.infrastructure.config.alilmTalk\n\nimport band.gosrock.common.annotation.Helper\nimport band.gosrock.common.exception.DuDoongDynamicException\nimport band.gosrock.common.helper.SpringEnvironmentHelper\nimport band.gosrock.infrastructure.config.alilmTalk.dto.AlimTalkOrderInfo\nimport band.gosrock.infrastructure.config.alilmTalk.dto.MessageDto\nimport band.gosrock.infrastructure.outer.api.alimTalk.client.NcpClient\nimport org.apache.commons.codec.binary.Base64\nimport org.springframework.beans.factory.annotation.Value\nimport java.time.Instant\nimport java.time.format.DateTimeFormatter\nimport javax.crypto.Mac\nimport javax.crypto.spec.SecretKeySpec\n\n@Helper\nclass NcpHelper(\n    private val ncpClient: NcpClient,\n    @Value(\"\\${ncp.service-id}\") private val serviceID: String,\n    @Value(\"\\${ncp.access-key}\") private val ncpAccessKey: String,\n    @Value(\"\\${ncp.secret-key}\") private val ncpSecretKey: String,\n    @Value(\"\\${ncp.plus-friend-id}\") private val plusFriendId: String,\n    private val springEnvironmentHelper: SpringEnvironmentHelper,\n) {\n    companion object {\n        const val space = \" \"\n        const val newLine = \"\\n\"\n        const val method = \"POST\"\n    }\n\n    // 주문 취소 알림톡 (아이템리스트+버튼)\n    fun sendCancelOrderAlimTalk(\n        to: String,\n        templateCode: String,\n        content: String,\n        headerContent: String,\n        orderInfo: AlimTalkOrderInfo,\n    ) {\n        if (!springEnvironmentHelper.isProdAndStagingProfile()) return\n        val timeStamp = Instant.now().toEpochMilli().toString()\n        val url = \"/alimtalk/v2/services/$serviceID/messages\"\n        val signature = makePostSignature(ncpAccessKey, ncpSecretKey, url, timeStamp)\n        val body = makeCancelOrderBody(templateCode, to, content, headerContent, orderInfo)\n        ncpClient.sendItemButtonAlimTalk(serviceID, ncpAccessKey, timeStamp, signature, body)\n    }\n\n    // 회원 가입 알림톡 (버튼)\n    fun sendButtonNcpAlimTalk(to: String, templateCode: String, content: String) {\n        if (!springEnvironmentHelper.isProdAndStagingProfile()) return\n        val timeStamp = Instant.now().toEpochMilli().toString()\n        val url = \"/alimtalk/v2/services/$serviceID/messages\"\n        val signature = makePostSignature(ncpAccessKey, ncpSecretKey, url, timeStamp)\n        val body = makeButtonBody(templateCode, to, content)\n        ncpClient.sendButtonAlimTalk(serviceID, ncpAccessKey, timeStamp, signature, body)\n    }\n\n    // 주문 성공 알림톡 (아이템리스트+버튼)\n    fun sendDoneOrderAlimTalk(\n        to: String,\n        templateCode: String,\n        content: String,\n        headerContent: String,\n        orderInfo: AlimTalkOrderInfo,\n    ) {\n        if (!springEnvironmentHelper.isProdAndStagingProfile()) return\n        val timeStamp = Instant.now().toEpochMilli().toString()\n        val url = \"/alimtalk/v2/services/$serviceID/messages\"\n        val signature = makePostSignature(ncpAccessKey, ncpSecretKey, url, timeStamp)\n        val body = makeDoneOrderBody(templateCode, to, content, headerContent, orderInfo)\n        ncpClient.sendItemButtonAlimTalk(serviceID, ncpAccessKey, timeStamp, signature, body)\n    }\n\n    // 주문서 전송 알림톡 (아이템리스트)\n    fun sendSettlementNcpAlimTalk(\n        to: String,\n        templateCode: String,\n        content: String,\n        headerContent: String,\n        email: String,\n        eventName: String,\n    ) {\n        if (!springEnvironmentHelper.isProdAndStagingProfile()) return\n        val timeStamp = Instant.now().toEpochMilli().toString()\n        val url = \"/alimtalk/v2/services/$serviceID/messages\"\n        val signature = makePostSignature(ncpAccessKey, ncpSecretKey, url, timeStamp)\n        val body = makeSettlementItemBody(templateCode, to, content, headerContent, email, eventName)\n        ncpClient.sendItemAlimTalk(serviceID, ncpAccessKey, timeStamp, signature, body)\n    }\n\n    fun makeDoneOrderBody(\n        templateCode: String,\n        to: String,\n        content: String,\n        headerContent: String,\n        orderInfo: AlimTalkOrderInfo,\n    ): MessageDto.AlimTalkItemButtonBody {\n        val item = makeOrderItem(orderInfo)\n        val buttons = makeDoneOrderButtons()\n        val message = MessageDto.AlimTalkItemButtonMessage(\n            to = to, content = content, headerContent = headerContent,\n            item = item, buttons = buttons,\n        )\n        return MessageDto.AlimTalkItemButtonBody(\n            plusFriendId = plusFriendId,\n            templateCode = templateCode,\n            messages = listOf(message),\n        )\n    }\n\n    fun makeCancelOrderBody(\n        templateCode: String,\n        to: String,\n        content: String,\n        headerContent: String,\n        orderInfo: AlimTalkOrderInfo,\n    ): MessageDto.AlimTalkItemButtonBody {\n        val item = makeOrderItem(orderInfo)\n        val buttons = makeCancelOrderButtons()\n        val message = MessageDto.AlimTalkItemButtonMessage(\n            to = to, content = content, headerContent = headerContent,\n            item = item, buttons = buttons,\n        )\n        return MessageDto.AlimTalkItemButtonBody(\n            plusFriendId = plusFriendId,\n            templateCode = templateCode,\n            messages = listOf(message),\n        )\n    }\n\n    fun makeSettlementItemBody(\n        templateCode: String,\n        to: String,\n        content: String,\n        headerContent: String,\n        email: String,\n        eventName: String,\n    ): MessageDto.AlimTalkItemBody {\n        val item = makeSettlementItem(email, eventName)\n        val message = MessageDto.AlimTalkItemMessage(\n            to = to, content = content, headerContent = headerContent, item = item,\n        )\n        return MessageDto.AlimTalkItemBody(\n            plusFriendId = plusFriendId,\n            templateCode = templateCode,\n            messages = listOf(message),\n        )\n    }\n\n    fun makeItemBody(\n        templateCode: String,\n        to: String,\n        content: String,\n        headerContent: String,\n        orderInfo: AlimTalkOrderInfo,\n    ): MessageDto.AlimTalkItemBody {\n        val item = makeOrderItem(orderInfo)\n        val message = MessageDto.AlimTalkItemMessage(\n            to = to, content = content, headerContent = headerContent, item = item,\n        )\n        return MessageDto.AlimTalkItemBody(\n            plusFriendId = plusFriendId,\n            templateCode = templateCode,\n            messages = listOf(message),\n        )\n    }\n\n    fun makeButtonBody(templateCode: String, to: String, content: String): MessageDto.AlimTalkButtonBody {\n        val buttons = makeSignUpButtons()\n        val message = MessageDto.AlimTalkButtonMessage(to = to, content = content, buttons = buttons)\n        return MessageDto.AlimTalkButtonBody(\n            plusFriendId = plusFriendId,\n            templateCode = templateCode,\n            messages = listOf(message),\n        )\n    }\n\n    fun makeSettlementItem(email: String, eventName: String): MessageDto.AlimTalkItem =\n        MessageDto.AlimTalkItem(\n            list = listOf(\n                MessageDto.Item(title = \"이메일 :\", description = email),\n                MessageDto.Item(title = \"이벤트 :\", description = eventName),\n            ),\n        )\n\n    fun makeOrderItem(orderInfo: AlimTalkOrderInfo): MessageDto.AlimTalkItem =\n        MessageDto.AlimTalkItem(\n            list = listOf(\n                MessageDto.Item(title = \"주문명 :\", description = orderInfo.name),\n                MessageDto.Item(title = \"수량 :\", description = orderInfo.quantity.toString()),\n                MessageDto.Item(title = \"가격 :\", description = orderInfo.money),\n                MessageDto.Item(\n                    title = \"주문일시 :\",\n                    description = orderInfo.createAt.format(DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm\")),\n                ),\n            ),\n        )\n\n    fun makeHomePageButton(): MessageDto.AlimTalkButton =\n        MessageDto.AlimTalkButton(\n            type = \"WL\", name = \"홈페이지 바로가기\",\n            linkMobile = \"https://dudoong.com/\", linkPc = \"https://dudoong.com/\",\n        )\n\n    fun makeAddChannelButton(): MessageDto.AlimTalkButton =\n        MessageDto.AlimTalkButton(type = \"AC\", name = \"채널 추가\")\n\n    fun makeMyPageButton(): MessageDto.AlimTalkButton =\n        MessageDto.AlimTalkButton(\n            type = \"WL\", name = \"마이페이지 바로가기\",\n            linkMobile = \"https://dudoong.com/mypage\", linkPc = \"https://dudoong.com/mypage\",\n        )\n\n    fun makeSignUpButtons(): List<MessageDto.AlimTalkButton> =\n        listOf(makeAddChannelButton(), makeHomePageButton())\n\n    fun makeDoneOrderButtons(): List<MessageDto.AlimTalkButton> =\n        listOf(makeMyPageButton())\n\n    fun makeCancelOrderButtons(): List<MessageDto.AlimTalkButton> =\n        listOf(makeHomePageButton())\n\n    fun makePostSignature(accessKey: String, secretKey: String, url: String, timeStamp: String): String {\n        return try {\n            val message = \"$method$space$url$newLine$timeStamp$newLine$accessKey\"\n            val signingKey = SecretKeySpec(secretKey.toByteArray(Charsets.UTF_8), \"HmacSHA256\")\n            val mac = Mac.getInstance(\"HmacSHA256\")\n            mac.init(signingKey)\n            val rawHmac = mac.doFinal(message.toByteArray(Charsets.UTF_8))\n            Base64.encodeBase64String(rawHmac)\n        } catch (ex: Exception) {\n            throw DuDoongDynamicException(0, \"400\", ex.message ?: \"\")\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/alilmTalk/dto/AlimTalkEventInfo.kt",
    "content": "package band.gosrock.infrastructure.config.alilmTalk.dto\n\ndata class AlimTalkEventInfo(\n    val hostName: String,\n    val eventName: String,\n)\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/alilmTalk/dto/AlimTalkOrderInfo.kt",
    "content": "package band.gosrock.infrastructure.config.alilmTalk.dto\n\nimport java.time.LocalDateTime\n\ndata class AlimTalkOrderInfo(\n    val name: String,\n    val quantity: Long,\n    val money: String,\n    val createAt: LocalDateTime,\n)\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/alilmTalk/dto/AlimTalkUserInfo.kt",
    "content": "package band.gosrock.infrastructure.config.alilmTalk.dto\n\ndata class AlimTalkUserInfo(\n    val userName: String,\n    val phoneNum: String,\n)\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/alilmTalk/dto/MessageDto.kt",
    "content": "package band.gosrock.infrastructure.config.alilmTalk.dto\n\nclass MessageDto {\n\n    data class AlimTalkItemButtonBody(\n        val plusFriendId: String? = null,\n        val templateCode: String? = null,\n        val messages: List<AlimTalkItemButtonMessage>? = null,\n    )\n\n    data class AlimTalkItemBody(\n        val plusFriendId: String? = null,\n        val templateCode: String? = null,\n        val messages: List<AlimTalkItemMessage>? = null,\n    )\n\n    data class AlimTalkButtonBody(\n        val plusFriendId: String? = null,\n        val templateCode: String? = null,\n        val messages: List<AlimTalkButtonMessage>? = null,\n    )\n\n    data class AlimTalkItemButtonMessage(\n        val to: String? = null,\n        val content: String? = null,\n        val headerContent: String? = null,\n        val item: AlimTalkItem? = null,\n        val buttons: List<AlimTalkButton>? = null,\n    )\n\n    data class AlimTalkItemMessage(\n        val to: String? = null,\n        val content: String? = null,\n        val headerContent: String? = null,\n        val item: AlimTalkItem? = null,\n    )\n\n    data class AlimTalkButtonMessage(\n        val to: String? = null,\n        val content: String? = null,\n        val buttons: List<AlimTalkButton>? = null,\n    )\n\n    data class AlimTalkButton(\n        val type: String? = null,\n        val name: String? = null,\n        val linkMobile: String? = null,\n        val linkPc: String? = null,\n    )\n\n    data class AlimTalkItem(\n        val list: List<Item>? = null,\n    )\n\n    data class Item(\n        val title: String? = null,\n        val description: String? = null,\n    )\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/feign/FeignCommonConfig.kt",
    "content": "package band.gosrock.infrastructure.config.feign\n\nimport band.gosrock.infrastructure.outer.api.BaseFeignClientPackage\nimport com.fasterxml.jackson.databind.DeserializationFeature\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.databind.SerializationFeature\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule\nimport feign.Logger\nimport feign.codec.Decoder\nimport feign.jackson.JacksonDecoder\nimport org.springframework.cloud.openfeign.EnableFeignClients\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\n\n@Configuration\n@EnableFeignClients(basePackageClasses = [BaseFeignClientPackage::class])\nclass FeignCommonConfig {\n\n    @Bean\n    fun feignDecoder(): Decoder = JacksonDecoder(customObjectMapper())\n\n    fun customObjectMapper(): ObjectMapper =\n        ObjectMapper().apply {\n            registerModule(JavaTimeModule())\n            configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)\n            configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)\n            configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false)\n        }\n\n    @Bean\n    fun feignLoggerLevel(): Logger.Level = Logger.Level.FULL\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/mail/dto/EmailEventInfo.kt",
    "content": "package band.gosrock.infrastructure.config.mail.dto\n\ndata class EmailEventInfo(\n    val hostName: String,\n    val eventName: String,\n)\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/mail/dto/EmailIssuedTicketInfo.kt",
    "content": "package band.gosrock.infrastructure.config.mail.dto\n\nimport java.time.LocalDateTime\n\ndata class EmailIssuedTicketInfo(\n    val issuedTicketNo: String,\n    val ticketName: String,\n    val createdAt: LocalDateTime,\n    val issuedTicketStatus: String,\n    val money: String,\n)\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/mail/dto/EmailOrderInfo.kt",
    "content": "package band.gosrock.infrastructure.config.mail.dto\n\nimport java.time.LocalDateTime\n\ndata class EmailOrderInfo(\n    val name: String,\n    val quantity: Long,\n    val money: String,\n    val createAt: LocalDateTime,\n)\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/mail/dto/EmailUserInfo.kt",
    "content": "package band.gosrock.infrastructure.config.mail.dto\n\ndata class EmailUserInfo(\n    val name: String,\n    val email: String,\n    val receiveAgree: Boolean,\n)\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/pdf/B64ImgReplacedElementFactory.kt",
    "content": "package band.gosrock.infrastructure.config.pdf\n\nimport com.lowagie.text.BadElementException\nimport com.lowagie.text.Image\nimport org.springframework.stereotype.Component\nimport org.w3c.dom.Element\nimport org.xhtmlrenderer.extend.FSImage\nimport org.xhtmlrenderer.extend.ReplacedElement\nimport org.xhtmlrenderer.extend.ReplacedElementFactory\nimport org.xhtmlrenderer.extend.UserAgentCallback\nimport org.xhtmlrenderer.layout.LayoutContext\nimport org.xhtmlrenderer.pdf.ITextFSImage\nimport org.xhtmlrenderer.pdf.ITextImageElement\nimport org.xhtmlrenderer.render.BlockBox\nimport org.xhtmlrenderer.simple.extend.FormSubmissionListener\nimport java.io.IOException\nimport java.util.Base64\n\n/**\n * 이미지 가져오는 방식수정\n * https://www.tothenew.com/blog/using-data-urls-for-embedding-images-in-flying-saucer-generated-pdfs/\n */\n@Component\nclass B64ImgReplacedElementFactory : ReplacedElementFactory {\n\n    override fun createReplacedElement(\n        c: LayoutContext,\n        box: BlockBox,\n        uac: UserAgentCallback,\n        cssWidth: Int,\n        cssHeight: Int,\n    ): ReplacedElement? {\n        val e = box.element ?: return null\n        val nodeName = e.nodeName\n        if (nodeName == \"img\") {\n            val attribute = e.getAttribute(\"src\")\n            val fsImage: FSImage? = try {\n                buildImage(attribute, uac)\n            } catch (e1: BadElementException) {\n                null\n            } catch (e1: IOException) {\n                null\n            }\n            if (fsImage != null) {\n                if (cssWidth != -1 || cssHeight != -1) {\n                    fsImage.scale(cssWidth, cssHeight)\n                }\n                return ITextImageElement(fsImage)\n            }\n        }\n        return null\n    }\n\n    @Throws(IOException::class, BadElementException::class)\n    protected fun buildImage(srcAttr: String, uac: UserAgentCallback): FSImage {\n        return if (srcAttr.startsWith(\"data:image/\")) {\n            val b64encoded = srcAttr.substring(srcAttr.indexOf(\"base64,\") + \"base64,\".length)\n            val decodedBytes = Base64.getDecoder().decode(b64encoded)\n            ITextFSImage(Image.getInstance(decodedBytes))\n        } else {\n            uac.getImageResource(srcAttr).image\n        }\n    }\n\n    override fun remove(e: Element) {}\n\n    override fun reset() {}\n\n    override fun setFormSubmissionListener(listener: FormSubmissionListener) {}\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/pdf/PdfRender.kt",
    "content": "package band.gosrock.infrastructure.config.pdf\n\nimport com.lowagie.text.DocumentException\nimport com.lowagie.text.pdf.BaseFont\nimport org.slf4j.LoggerFactory\nimport org.springframework.core.io.ClassPathResource\nimport org.springframework.stereotype.Component\nimport org.xhtmlrenderer.pdf.ITextRenderer\nimport java.io.ByteArrayOutputStream\nimport java.io.File\nimport java.io.IOException\n\n@Component\nclass PdfRender(\n    private val b64ImgReplacedElementFactory: B64ImgReplacedElementFactory,\n) {\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    @Throws(DocumentException::class, IOException::class)\n    fun generatePdfFromHtml(html: String): ByteArrayOutputStream {\n        val outputFolder = System.getProperty(\"user.home\") + File.separator + \"thymeleaf.pdf\"\n        val outputStream = ByteArrayOutputStream()\n        log.info(outputFolder)\n        val renderer = ITextRenderer()\n        val sharedContext = renderer.sharedContext\n        sharedContext.isPrint = true\n        sharedContext.isInteractive = false\n        sharedContext.setReplacedElementFactory(b64ImgReplacedElementFactory)\n        sharedContext.textRenderer.setSmoothingThreshold(0f)\n\n        renderer.fontResolver.addFont(\n            ClassPathResource(\"/templates/NanumBarunGothic.ttf\").url.toString(),\n            BaseFont.IDENTITY_H,\n            BaseFont.EMBEDDED,\n        )\n        renderer.setDocumentFromString(html)\n        renderer.layout()\n        renderer.createPDF(outputStream)\n        outputStream.close()\n        return outputStream\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/redis/RedisCacheConfig.kt",
    "content": "package band.gosrock.infrastructure.config.redis\n\nimport java.time.Duration\nimport org.springframework.cache.CacheManager\nimport org.springframework.cache.annotation.EnableCaching\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\nimport org.springframework.context.annotation.Primary\nimport org.springframework.data.redis.cache.RedisCacheConfiguration\nimport org.springframework.data.redis.cache.RedisCacheManager\nimport org.springframework.data.redis.connection.RedisConnectionFactory\nimport org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer\nimport org.springframework.data.redis.serializer.RedisSerializationContext\nimport org.springframework.data.redis.serializer.StringRedisSerializer\n\n@EnableCaching\n@Configuration\nclass RedisCacheConfig {\n\n    @Bean\n    @Primary\n    fun redisCacheManager(cf: RedisConnectionFactory): CacheManager {\n        val redisCacheConfiguration =\n            RedisCacheConfiguration.defaultCacheConfig()\n                .serializeKeysWith(\n                    RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()),\n                )\n                .serializeValuesWith(\n                    RedisSerializationContext.SerializationPair.fromSerializer(GenericJackson2JsonRedisSerializer()),\n                )\n                .entryTtl(Duration.ofHours(1L))\n\n        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf)\n            .cacheDefaults(redisCacheConfiguration)\n            .build()\n    }\n\n    @Bean\n    fun oidcCacheManager(cf: RedisConnectionFactory): CacheManager {\n        val redisCacheConfiguration =\n            RedisCacheConfiguration.defaultCacheConfig()\n                .serializeKeysWith(\n                    RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()),\n                )\n                .serializeValuesWith(\n                    RedisSerializationContext.SerializationPair.fromSerializer(GenericJackson2JsonRedisSerializer()),\n                )\n                .entryTtl(Duration.ofDays(7L))\n\n        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf)\n            .cacheDefaults(redisCacheConfiguration)\n            .build()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/redis/RedisConfig.kt",
    "content": "package band.gosrock.infrastructure.config.redis\n\nimport java.time.Duration\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\nimport org.springframework.data.redis.connection.RedisConnectionFactory\nimport org.springframework.data.redis.connection.RedisStandaloneConfiguration\nimport org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration\nimport org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory\nimport org.springframework.data.redis.core.RedisKeyValueAdapter\nimport org.springframework.data.redis.repository.configuration.EnableRedisRepositories\n\n@EnableRedisRepositories(\n    basePackages = [\"band.gosrock\"],\n    enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP,\n)\n@Configuration\nclass RedisConfig(\n    @Value(\"\\${spring.redis.host}\") private val redisHost: String,\n    @Value(\"\\${spring.redis.port}\") private val redisPort: Int,\n    @Value(\"\\${spring.redis.password}\") private val redisPassword: String,\n) {\n    @Bean\n    fun redisConnectionFactory(): RedisConnectionFactory {\n        val redisConfig = RedisStandaloneConfiguration(redisHost, redisPort)\n\n        if (redisPassword.isNotBlank()) {\n            redisConfig.setPassword(redisPassword)\n        }\n\n        val clientConfig =\n            LettuceClientConfiguration.builder()\n                .commandTimeout(Duration.ofSeconds(1))\n                .shutdownTimeout(Duration.ZERO)\n                .build()\n        return LettuceConnectionFactory(redisConfig, clientConfig)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/redis/RedissonConfig.kt",
    "content": "package band.gosrock.infrastructure.config.redis\n\nimport io.github.bucket4j.distributed.proxy.ProxyManager\nimport io.github.bucket4j.grid.jcache.JCacheProxyManager\nimport javax.cache.Cache\nimport javax.cache.CacheManager\nimport javax.cache.Caching\nimport org.redisson.Redisson\nimport org.redisson.api.RedissonClient\nimport org.redisson.config.Config\nimport org.redisson.jcache.configuration.RedissonConfiguration\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\n\n@Configuration\nclass RedissonConfig(\n    @Value(\"\\${spring.redis.host}\") private val redisHost: String,\n    @Value(\"\\${spring.redis.port}\") private val redisPort: Int,\n) {\n    companion object {\n        private const val REDISSON_HOST_PREFIX = \"redis://\"\n    }\n\n    @Bean\n    fun redissonClient(): RedissonClient {\n        val config = Config()\n        config.useSingleServer().setAddress(\"$REDISSON_HOST_PREFIX$redisHost:$redisPort\")\n        return Redisson.create(config)\n    }\n\n    /** for bucket4j */\n    @Bean\n    fun cacheManager(redissonClient: RedissonClient): CacheManager {\n        val manager = Caching.getCachingProvider().cacheManager\n        // 1-arg getCache (no type checking) to match original Java behavior\n        val bucket4j = manager.getCache<Any, Any>(\"bucket4j\")\n        if (bucket4j == null) {\n            manager.createCache(\"bucket4j\", RedissonConfiguration.fromInstance<Any, Any>(redissonClient))\n        }\n        return manager\n    }\n\n    /** for bucket4j */\n    @Bean\n    @Suppress(\"UNCHECKED_CAST\")\n    fun proxyManager(cacheManager: CacheManager): ProxyManager<String> =\n        JCacheProxyManager(cacheManager.getCache<Any, Any>(\"bucket4j\") as Cache<String, ByteArray>)\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/s3/ImageFileExtension.kt",
    "content": "package band.gosrock.infrastructure.config.s3\n\nenum class ImageFileExtension(val uploadExtension: String) {\n    JPEG(\"jpeg\"),\n    JPG(\"jpeg\"),\n    PNG(\"png\"),\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/s3/ImageUrlDto.kt",
    "content": "package band.gosrock.infrastructure.config.s3\n\ndata class ImageUrlDto(\n    val url: String,\n    val key: String,\n    val baseUrl: String? = null,\n) {\n    companion object {\n        @JvmStatic\n        fun of(url: String, key: String): ImageUrlDto = ImageUrlDto(url = url, key = key)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/s3/S3Config.kt",
    "content": "package band.gosrock.infrastructure.config.s3\n\nimport com.amazonaws.auth.AWSStaticCredentialsProvider\nimport com.amazonaws.auth.BasicAWSCredentials\nimport com.amazonaws.regions.Regions\nimport com.amazonaws.services.s3.AmazonS3\nimport com.amazonaws.services.s3.AmazonS3ClientBuilder\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\n\n@Configuration\nclass S3Config(\n    @Value(\"\\${aws.access-key}\") private val accessKey: String,\n    @Value(\"\\${aws.secret-key}\") private val secretKey: String,\n) {\n    @Bean\n    fun getS3ClientBean(): AmazonS3 {\n        val credentials = BasicAWSCredentials(accessKey, secretKey)\n        return AmazonS3ClientBuilder.standard()\n            .withCredentials(AWSStaticCredentialsProvider(credentials))\n            .withRegion(Regions.AP_NORTHEAST_2)\n            .build()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/s3/S3PrivateFileService.kt",
    "content": "package band.gosrock.infrastructure.config.s3\n\nimport com.amazonaws.services.s3.AmazonS3\nimport com.amazonaws.services.s3.model.ObjectMetadata\nimport java.io.ByteArrayInputStream\nimport java.io.ByteArrayOutputStream\nimport java.io.IOException\nimport org.slf4j.LoggerFactory\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.stereotype.Service\n\n@Service\nclass S3PrivateFileService(\n    private val amazonS3: AmazonS3,\n    @Value(\"\\${aws.s3.private-bucket}\") private val bucket: String,\n    @Value(\"\\${aws.s3.base-url}\") private val baseUrl: String,\n) {\n    private val log = LoggerFactory.getLogger(S3PrivateFileService::class.java)\n\n    companion object {\n        private const val eventOrdersExcelFileName = \"eventOrders.xlsx\"\n        private const val eventSettlementPdfFileName = \"eventSettlement.pdf\"\n    }\n\n    fun eventOrdersExcelUpload(eventId: Long, outputStream: ByteArrayOutputStream): String {\n        val bytes = outputStream.toByteArray()\n        val inputStream = ByteArrayInputStream(bytes)\n        val fileKey = eventOrdersExcelGetKey(eventId)\n        amazonS3.putObject(bucket, fileKey, inputStream, getExcelObjectMetadata(bytes.size))\n        return fileKey\n    }\n\n    fun eventSettlementPdfUpload(eventId: Long, outputStream: ByteArrayOutputStream): String {\n        val bytes = outputStream.toByteArray()\n        val inputStream = ByteArrayInputStream(bytes)\n        val fileKey = getEventSettlementPdfKey(eventId)\n        amazonS3.putObject(bucket, fileKey, inputStream, getPdfObjectMetadata(bytes.size))\n        return fileKey\n    }\n\n    private fun eventOrdersExcelGetKey(eventId: Long): String =\n        \"$baseUrl/event/$eventId/$eventOrdersExcelFileName\"\n\n    private fun getEventSettlementPdfKey(eventId: Long): String =\n        \"$baseUrl/event/$eventId/$eventSettlementPdfFileName\"\n\n    private fun getExcelObjectMetadata(contentLength: Int): ObjectMetadata {\n        val objectMetadata = ObjectMetadata()\n        objectMetadata.contentType = \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n        objectMetadata.contentLength = contentLength.toLong()\n        return objectMetadata\n    }\n\n    private fun getPdfObjectMetadata(contentLength: Int): ObjectMetadata {\n        val objectMetadata = ObjectMetadata()\n        objectMetadata.contentType = \"application/pdf\"\n        objectMetadata.contentLength = contentLength.toLong()\n        return objectMetadata\n    }\n\n    fun downloadEventSettlementPdf(eventId: Long): ByteArray {\n        val obj = amazonS3.getObject(bucket, getEventSettlementPdfKey(eventId))\n        return try {\n            obj.objectContent.readAllBytes()\n        } catch (e: IOException) {\n            throw RuntimeException(e)\n        }\n    }\n\n    fun downloadEventOrdersExcel(eventId: Long): ByteArray {\n        val obj = amazonS3.getObject(bucket, eventOrdersExcelGetKey(eventId))\n        return try {\n            obj.objectContent.readAllBytes()\n        } catch (e: IOException) {\n            throw RuntimeException(e)\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/s3/S3UploadPresignedUrlService.kt",
    "content": "package band.gosrock.infrastructure.config.s3\n\nimport band.gosrock.common.exception.BadFileExtensionException\nimport com.amazonaws.HttpMethod\nimport com.amazonaws.services.s3.AmazonS3\nimport com.amazonaws.services.s3.Headers\nimport com.amazonaws.services.s3.model.CannedAccessControlList\nimport com.amazonaws.services.s3.model.GeneratePresignedUrlRequest\nimport java.util.Date\nimport java.util.UUID\nimport org.slf4j.LoggerFactory\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.stereotype.Service\n\n@Service\nclass S3UploadPresignedUrlService(\n    private val amazonS3: AmazonS3,\n    @Value(\"\\${aws.s3.bucket}\") private val bucket: String,\n    @Value(\"\\${aws.s3.base-url}\") private val baseUrl: String,\n) {\n    private val log = LoggerFactory.getLogger(S3UploadPresignedUrlService::class.java)\n\n    fun forUser(userId: Long, fileExtension: ImageFileExtension): ImageUrlDto {\n        val fixedFileExtension = fileExtension.uploadExtension\n        val fileName = getForUserFileName(userId, fixedFileExtension)\n        log.info(fileName)\n        val url = amazonS3.generatePresignedUrl(getGeneratePreSignedUrlRequest(bucket, fileName, fixedFileExtension))\n        return ImageUrlDto.of(url.toString(), fileName)\n    }\n\n    fun forHost(hostId: Long, fileExtension: ImageFileExtension): ImageUrlDto {\n        val fixedFileExtension = fileExtension.uploadExtension\n        val fileName = getForHostFileName(hostId, fixedFileExtension)\n        log.info(fileName)\n        val url = amazonS3.generatePresignedUrl(getGeneratePreSignedUrlRequest(bucket, fileName, fixedFileExtension))\n        return ImageUrlDto.of(url.toString(), fileName)\n    }\n\n    fun forEvent(eventId: Long, fileExtension: ImageFileExtension): ImageUrlDto {\n        val fixedFileExtension = fileExtension.uploadExtension\n        val fileName = getForEventFileName(eventId, fixedFileExtension)\n        log.info(fileName)\n        val url = amazonS3.generatePresignedUrl(getGeneratePreSignedUrlRequest(bucket, fileName, fixedFileExtension))\n        return ImageUrlDto.of(url.toString(), fileName)\n    }\n\n    private fun getForUserFileName(userId: Long, fileExtension: String): String =\n        \"$baseUrl/user/$userId/${UUID.randomUUID()}.$fileExtension\"\n\n    private fun getForHostFileName(hostId: Long, fileExtension: String): String =\n        \"$baseUrl/host/$hostId/${UUID.randomUUID()}.$fileExtension\"\n\n    private fun getForEventFileName(eventId: Long, fileExtension: String): String =\n        \"$baseUrl/event/$eventId/${UUID.randomUUID()}.$fileExtension\"\n\n    private fun getGeneratePreSignedUrlRequest(\n        bucket: String,\n        fileName: String,\n        fileExtension: String,\n    ): GeneratePresignedUrlRequest {\n        val generatePresignedUrlRequest =\n            GeneratePresignedUrlRequest(bucket, fileName)\n                .withMethod(HttpMethod.PUT)\n                .withKey(fileName)\n                .withContentType(\"image/$fileExtension\")\n                .withExpiration(getPreSignedUrlExpiration())\n        generatePresignedUrlRequest.addRequestParameter(\n            Headers.S3_CANNED_ACL,\n            CannedAccessControlList.PublicRead.toString(),\n        )\n        return generatePresignedUrlRequest\n    }\n\n    private fun getPreSignedUrlExpiration(): Date {\n        val expiration = Date()\n        var expTimeMillis = expiration.time\n        // 3분\n        expTimeMillis += 1000 * 60 * 3\n        expiration.time = expTimeMillis\n        return expiration\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/ses/AwsSesConfig.kt",
    "content": "package band.gosrock.infrastructure.config.ses\n\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\nimport software.amazon.awssdk.auth.credentials.AwsBasicCredentials\nimport software.amazon.awssdk.auth.credentials.StaticCredentialsProvider\nimport software.amazon.awssdk.regions.Region\nimport software.amazon.awssdk.services.ses.SesClient\n\n@Configuration\nclass AwsSesConfig(\n    @Value(\"\\${aws.access-key}\") private val accessKey: String,\n    @Value(\"\\${aws.secret-key}\") private val secretKey: String,\n) {\n    @Bean\n    fun sesClient(): SesClient {\n        val awsBasicCredentials = AwsBasicCredentials.create(accessKey, secretKey)\n        return SesClient.builder()\n            .credentialsProvider(StaticCredentialsProvider.create(awsBasicCredentials))\n            .region(Region.US_WEST_2)\n            .build()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/ses/AwsSesUtils.kt",
    "content": "package band.gosrock.infrastructure.config.ses\n\nimport band.gosrock.infrastructure.config.mail.dto.EmailUserInfo\nimport java.io.ByteArrayOutputStream\nimport java.nio.ByteBuffer\nimport java.util.Properties\nimport jakarta.activation.DataHandler\nimport jakarta.mail.MessagingException\nimport jakarta.mail.Session\nimport jakarta.mail.internet.InternetAddress\nimport jakarta.mail.internet.MimeBodyPart\nimport jakarta.mail.internet.MimeMessage\nimport jakarta.mail.internet.MimeMessage.RecipientType\nimport jakarta.mail.internet.MimeMultipart\nimport jakarta.mail.util.ByteArrayDataSource\nimport org.slf4j.LoggerFactory\nimport org.springframework.stereotype.Component\nimport org.thymeleaf.context.Context\nimport org.thymeleaf.spring6.SpringTemplateEngine\nimport software.amazon.awssdk.core.SdkBytes\nimport software.amazon.awssdk.services.ses.SesClient\nimport software.amazon.awssdk.services.ses.model.Body\nimport software.amazon.awssdk.services.ses.model.Content\nimport software.amazon.awssdk.services.ses.model.Destination\nimport software.amazon.awssdk.services.ses.model.Message\nimport software.amazon.awssdk.services.ses.model.RawMessage\nimport software.amazon.awssdk.services.ses.model.SendEmailRequest\nimport software.amazon.awssdk.services.ses.model.SendRawEmailRequest\n\n@Component\nclass AwsSesUtils(\n    private val sesClient: SesClient,\n    private val templateEngine: SpringTemplateEngine,\n) {\n    private val log = LoggerFactory.getLogger(AwsSesUtils::class.java)\n\n    fun singleEmailRequest(emailUserInfo: EmailUserInfo, subject: String, template: String, context: Context) {\n        if (!emailUserInfo.receiveAgree) return\n        val html = templateEngine.process(template, context)\n        val sendEmailRequestBuilder = SendEmailRequest.builder()\n        sendEmailRequestBuilder.destination(Destination.builder().toAddresses(emailUserInfo.email).build())\n        sendEmailRequestBuilder\n            .message(newMessage(subject, html))\n            .source(\"life@dudoong.com\")\n            .build()\n        sesClient.sendEmail(sendEmailRequestBuilder.build())\n    }\n\n    private fun newMessage(subject: String, html: String): Message {\n        val content = Content.builder().data(subject).build()\n        return Message.builder()\n            .subject(content)\n            .body(Body.builder().html { it.data(html) }.build())\n            .build()\n    }\n\n    @Throws(MessagingException::class)\n    fun sendRawEmails(sendRawEmailDto: SendRawEmailDto) {\n        val session = Session.getDefaultInstance(Properties())\n        val message = MimeMessage(session)\n        setRawEmailBaseInfo(sendRawEmailDto, message)\n        val msg = MimeMultipart(\"mixed\")\n        message.setContent(msg)\n        setBodyHtml(sendRawEmailDto, msg)\n        setAttachments(sendRawEmailDto, msg)\n        try {\n            sesClient.sendRawEmail(buildSendRawEmailRequest(message))\n        } catch (ex: Exception) {\n            log.info(ex.toString())\n            ex.printStackTrace()\n        }\n    }\n\n    private fun buildSendRawEmailRequest(message: MimeMessage): SendRawEmailRequest {\n        val outputStream = ByteArrayOutputStream()\n        message.writeTo(outputStream)\n        val rawMessage =\n            RawMessage.builder()\n                .data(SdkBytes.fromByteBuffer(ByteBuffer.wrap(outputStream.toByteArray())))\n                .build()\n        return SendRawEmailRequest.builder().rawMessage(rawMessage).build()\n    }\n\n    private fun setAttachments(sendRawEmailDto: SendRawEmailDto, msg: MimeMultipart) {\n        sendRawEmailDto.rawEmailAttachments.forEach { setAttachmentToMessage(msg, it) }\n    }\n\n    @Throws(MessagingException::class)\n    private fun setBodyHtml(sendRawEmailDto: SendRawEmailDto, msg: MimeMultipart) {\n        val htmlPart = MimeBodyPart()\n        htmlPart.setContent(sendRawEmailDto.bodyHtml, \"text/html; charset=UTF-8\")\n        msg.addBodyPart(htmlPart)\n    }\n\n    private fun setAttachmentToMessage(msg: MimeMultipart, rawEmailAttachmentDto: RawEmailAttachmentDto) {\n        try {\n            val att = MimeBodyPart()\n            val fds = ByteArrayDataSource(rawEmailAttachmentDto.fileBytes, rawEmailAttachmentDto.type)\n            att.dataHandler = DataHandler(fds)\n            att.fileName = rawEmailAttachmentDto.fileName\n            msg.addBodyPart(att)\n        } catch (e: Exception) {\n            log.info(e.toString())\n            e.printStackTrace()\n        }\n    }\n\n    @Throws(MessagingException::class)\n    private fun setRawEmailBaseInfo(sendRawEmailDto: SendRawEmailDto, message: MimeMessage) {\n        message.setSubject(sendRawEmailDto.subject, \"UTF-8\")\n        message.setFrom(InternetAddress(sendRawEmailDto.sender))\n        message.setRecipients(RecipientType.TO, InternetAddress.parse(sendRawEmailDto.recipient))\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/ses/RawEmailAttachmentDto.kt",
    "content": "package band.gosrock.infrastructure.config.ses\n\n/** ses 로 raw email 보낼때 첨부파일의 내용을 지정할 수 있다. */\nclass RawEmailAttachmentDto(\n    /** file type ex : application/pdf */\n    val type: String,\n    /**\n     * will be ByteArrayDataSource\n     * @see javax.mail.util.ByteArrayDataSource\n     */\n    val fileBytes: ByteArray,\n    /** 사용자가 이메일을 받았을 때 뜰 첨부파일이름. ex : 이벤트_정산서.pdf */\n    val fileName: String,\n)\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/ses/SendRawEmailDto.kt",
    "content": "package band.gosrock.infrastructure.config.ses\n\nclass SendRawEmailDto(\n    val recipient: String,\n    val subject: String,\n    val bodyHtml: String,\n) {\n    // Replace sender@example.com with your \"From\" address.\n    val sender: String = \"공연 정산관리팀 <support@dudoong.com>\"\n    val rawEmailAttachments: MutableList<RawEmailAttachmentDto> = mutableListOf()\n\n    fun addEmailAttachments(rawEmailAttachment: RawEmailAttachmentDto) {\n        rawEmailAttachments.add(rawEmailAttachment)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/slack/SlackAsyncErrorSender.kt",
    "content": "package band.gosrock.infrastructure.config.slack\n\nimport com.slack.api.model.block.Blocks\nimport com.slack.api.model.block.Blocks.divider\nimport com.slack.api.model.block.Blocks.section\nimport com.slack.api.model.block.LayoutBlock\nimport com.slack.api.model.block.composition.BlockCompositions.plainText\nimport com.slack.api.model.block.composition.MarkdownTextObject\nimport org.slf4j.MDC\nimport org.springframework.stereotype.Component\n\n@Component\nclass SlackAsyncErrorSender(\n    private val slackProvider: SlackErrorNotificationProvider,\n) {\n    fun execute(name: String, throwable: Throwable, params: Array<Any>) {\n        val traceId = MDC.get(\"traceId\") ?: \"no-trace\"\n        val layoutBlocks = mutableListOf<LayoutBlock>()\n        layoutBlocks.add(Blocks.header { it.text(plainText(\"비동기 에러 알림\")) })\n        layoutBlocks.add(divider())\n\n        val traceIdMarkdown = MarkdownTextObject.builder().text(\"* Trace ID :*\\n`$traceId`\").build()\n        val errorUserIdMarkdown = MarkdownTextObject.builder().text(\"* 메소드 이름 :*\\n$name\").build()\n        layoutBlocks.add(section { it.fields(listOf(traceIdMarkdown, errorUserIdMarkdown)) })\n\n        val errorUserIpMarkdown = MarkdownTextObject.builder()\n            .text(\"* 요청 파라미터 :*\\n${getParamsToString(params)}\")\n            .build()\n        layoutBlocks.add(section { it.fields(listOf(errorUserIpMarkdown)) })\n\n        layoutBlocks.add(divider())\n        val errorStack = slackProvider.getErrorStack(throwable)\n        val message = throwable.toString()\n        val errorNameMarkdown = MarkdownTextObject.builder().text(\"* Message :*\\n$message\").build()\n        val errorStackMarkdown = MarkdownTextObject.builder().text(\"* Stack Trace :*\\n$errorStack\").build()\n        layoutBlocks.add(section { it.fields(listOf(errorNameMarkdown, errorStackMarkdown)) })\n\n        slackProvider.sendNotification(layoutBlocks)\n    }\n\n    private fun getParamsToString(params: Array<Any>): String =\n        params.joinToString(\"\") { it.toString() }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/slack/SlackErrorNotificationProvider.kt",
    "content": "package band.gosrock.infrastructure.config.slack\n\nimport com.slack.api.model.block.LayoutBlock\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.scheduling.annotation.Async\nimport org.springframework.stereotype.Component\n\n@Component\nclass SlackErrorNotificationProvider(\n    private val slackHelper: SlackHelper,\n    @Value(\"\\${slack.webhook.id}\") private val channelId: String,\n) {\n    private val maxLen = 500\n\n    fun getErrorStack(throwable: Throwable): String {\n        val exceptionAsString = throwable.stackTrace.contentToString()\n        val cutLength = minOf(exceptionAsString.length, maxLen)\n        return exceptionAsString.substring(0, cutLength)\n    }\n\n    @Async\n    fun sendNotification(layoutBlocks: List<LayoutBlock>) {\n        slackHelper.sendNotification(channelId, layoutBlocks)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/slack/SlackHelper.kt",
    "content": "package band.gosrock.infrastructure.config.slack\n\nimport band.gosrock.common.helper.SpringEnvironmentHelper\nimport com.slack.api.methods.MethodsClient\nimport com.slack.api.methods.SlackApiException\nimport com.slack.api.methods.request.chat.ChatPostMessageRequest\nimport com.slack.api.model.block.LayoutBlock\nimport org.slf4j.LoggerFactory\nimport org.springframework.stereotype.Component\nimport java.io.IOException\n\n@Component\nclass SlackHelper(\n    private val springEnvironmentHelper: SpringEnvironmentHelper,\n    private val methodsClient: MethodsClient,\n) {\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    fun sendNotification(channelId: String, layoutBlocks: List<LayoutBlock>) {\n        if (!springEnvironmentHelper.isProdAndStagingProfile()) return\n        val request = ChatPostMessageRequest.builder()\n            .channel(channelId)\n            .text(\"\")\n            .blocks(layoutBlocks)\n            .build()\n        try {\n            methodsClient.chatPostMessage(request)\n        } catch (e: SlackApiException) {\n            log.error(e.toString())\n        } catch (e: IOException) {\n            log.error(e.toString())\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/slack/SlackMessageProvider.kt",
    "content": "package band.gosrock.infrastructure.config.slack\n\nimport com.slack.api.Slack\nimport com.slack.api.webhook.Payload\nimport org.apache.commons.codec.binary.StringUtils\nimport org.slf4j.LoggerFactory\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.stereotype.Service\nimport java.io.IOException\nimport java.net.UnknownHostException\n\n@Service\nclass SlackMessageProvider(\n    @Value(\"\\${slack.webhook.username}\") private val username: String,\n    @Value(\"\\${slack.webhook.icon-url}\") private val iconUrl: String,\n) {\n    private val log = LoggerFactory.getLogger(javaClass)\n\n    /** 이벤트 핸들러 자체에서 비동기로 실행하기 때문에 @Async 어노테이션 지움 */\n    fun sendMessage(url: String?, text: String) {\n        if (url == null) return\n        try {\n            doSend(url, text)\n        } catch (e: Exception) {\n            // ignored\n        }\n    }\n\n    /** 호스트가 존재하는 지 확인하기 위해 동기로 처리 */\n    @Throws(UnknownHostException::class)\n    fun register(url: String) {\n        val text = \"두둥 슬랙 알림이 성공적으로 등록되었습니다!\"\n        doSend(url, text)\n    }\n\n    @Throws(UnknownHostException::class)\n    private fun doSend(url: String, text: String) {\n        val slack = Slack.getInstance()\n        val payload = Payload.builder().text(text).username(username).iconUrl(iconUrl).build()\n        try {\n            val responseBody = slack.send(url, payload).body\n            if (!StringUtils.equals(responseBody, \"ok\")) {\n                throw UnknownHostException(\"올바른 슬랙 URL이 아닙니다.\")\n            }\n        } catch (e: UnknownHostException) {\n            throw e\n        } catch (e: IOException) {\n            log.error(e.message, e)\n            throw RuntimeException(e)\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/slack/SlackServiceNotificationProvider.kt",
    "content": "package band.gosrock.infrastructure.config.slack\n\nimport com.slack.api.model.block.LayoutBlock\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.stereotype.Component\n\n@Component\nclass SlackServiceNotificationProvider(\n    private val slackHelper: SlackHelper,\n    @Value(\"\\${slack.webhook.service-alarm-channel}\") private val channelId: String,\n) {\n    fun sendNotification(layoutBlocks: List<LayoutBlock>) {\n        slackHelper.sendNotification(channelId, layoutBlocks)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/config/slack/config/SlackApiConfig.kt",
    "content": "package band.gosrock.infrastructure.config.slack.config\n\nimport com.slack.api.Slack\nimport com.slack.api.methods.MethodsClient\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\n\n@Configuration\nclass SlackApiConfig(\n    @Value(\"\\${slack.webhook.token}\") private val token: String,\n) {\n    @Bean\n    fun getClient(): MethodsClient {\n        val slackClient = Slack.getInstance()\n        return slackClient.methods(token)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/BaseFeignClientPackage.kt",
    "content": "package band.gosrock.infrastructure.outer.api\n\ninterface BaseFeignClientPackage\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/alimTalk/client/NcpClient.kt",
    "content": "package band.gosrock.infrastructure.outer.api.alimTalk.client\n\nimport band.gosrock.infrastructure.config.alilmTalk.dto.MessageDto\nimport band.gosrock.infrastructure.outer.api.alimTalk.config.NcpConfig\nimport feign.Headers\nimport org.springframework.cloud.openfeign.FeignClient\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestBody\nimport org.springframework.web.bind.annotation.RequestHeader\n\n@FeignClient(\n    name = \"NcpClient\",\n    url = \"https://sens.apigw.ntruss.com\",\n    configuration = [NcpConfig::class],\n)\n@Headers(\"Content-Type: application/json; charset=UTF-8\")\ninterface NcpClient {\n\n    // 주문 취소 알림톡 (아이템리스트+버튼)\n    @PostMapping(\n        path = [\"/alimtalk/v2/services/{serviceId}/messages\"],\n        consumes = [\"application/json; charset=UTF-8\"],\n    )\n    fun sendItemButtonAlimTalk(\n        @PathVariable(\"serviceId\") serviceId: String,\n        @RequestHeader(\"x-ncp-iam-access-key\") ncpAccessKey: String,\n        @RequestHeader(\"x-ncp-apigw-timestamp\") timeStamp: String,\n        @RequestHeader(\"x-ncp-apigw-signature-v2\") signature: String,\n        @RequestBody alimTalkItemButtonBody: MessageDto.AlimTalkItemButtonBody,\n    )\n\n    // 주문 성공 알림톡 (아이템리스트)\n    @PostMapping(\n        path = [\"/alimtalk/v2/services/{serviceId}/messages\"],\n        consumes = [\"application/json; charset=UTF-8\"],\n    )\n    fun sendItemAlimTalk(\n        @PathVariable(\"serviceId\") serviceId: String,\n        @RequestHeader(\"x-ncp-iam-access-key\") ncpAccessKey: String,\n        @RequestHeader(\"x-ncp-apigw-timestamp\") timeStamp: String,\n        @RequestHeader(\"x-ncp-apigw-signature-v2\") signature: String,\n        @RequestBody alimTalkItemBody: MessageDto.AlimTalkItemBody,\n    )\n\n    // 회원 가입 알림톡 (버튼)\n    @PostMapping(\n        path = [\"/alimtalk/v2/services/{serviceId}/messages\"],\n        consumes = [\"application/json; charset=UTF-8\"],\n    )\n    fun sendButtonAlimTalk(\n        @PathVariable(\"serviceId\") serviceId: String,\n        @RequestHeader(\"x-ncp-iam-access-key\") ncpAccessKey: String,\n        @RequestHeader(\"x-ncp-apigw-timestamp\") timeStamp: String,\n        @RequestHeader(\"x-ncp-apigw-signature-v2\") signature: String,\n        @RequestBody alimTalkButtonBody: MessageDto.AlimTalkButtonBody,\n    )\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/alimTalk/config/NcpConfig.kt",
    "content": "package band.gosrock.infrastructure.outer.api.alimTalk.config\n\nimport feign.RequestInterceptor\nimport feign.RequestTemplate\nimport feign.codec.ErrorDecoder\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Import\n\n@Import(NcpErrorDecoder::class)\nclass NcpConfig {\n    @Bean\n    @ConditionalOnMissingBean(value = [ErrorDecoder::class])\n    fun commonFeignErrorDecoder(): NcpErrorDecoder = NcpErrorDecoder()\n\n    @Bean\n    fun basicAuthRequestInterceptor(): RequestInterceptor = ColonInterceptor()\n\n    class ColonInterceptor : RequestInterceptor {\n        override fun apply(template: RequestTemplate) {\n            template.uri(template.path().replace(\"%3A\", \":\"))\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/alimTalk/config/NcpErrorDecoder.kt",
    "content": "package band.gosrock.infrastructure.outer.api.alimTalk.config\n\nimport band.gosrock.common.exception.OtherServerBadRequestException\nimport band.gosrock.common.exception.OtherServerForbiddenException\nimport band.gosrock.common.exception.OtherServerInternalSeverErrorException\nimport band.gosrock.common.exception.OtherServerNotFoundException\nimport band.gosrock.common.exception.OtherServerUnauthorizedException\nimport feign.FeignException\nimport feign.Response\nimport feign.codec.ErrorDecoder\n\nclass NcpErrorDecoder : ErrorDecoder {\n    override fun decode(methodKey: String, response: Response): Exception {\n        if (response.status() >= 400) {\n            when (response.status()) {\n                401 -> throw OtherServerUnauthorizedException.EXCEPTION\n                403 -> throw OtherServerForbiddenException.EXCEPTION\n                404 -> throw OtherServerNotFoundException.EXCEPTION\n                500 -> throw OtherServerInternalSeverErrorException.EXCEPTION\n                else -> throw OtherServerBadRequestException.EXCEPTION\n            }\n        }\n        return FeignException.errorStatus(methodKey, response)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/oauth/client/KakaoInfoClient.kt",
    "content": "package band.gosrock.infrastructure.outer.api.oauth.client\n\nimport band.gosrock.infrastructure.outer.api.oauth.config.KakaoInfoConfig\nimport band.gosrock.infrastructure.outer.api.oauth.dto.KakaoInformationResponse\nimport band.gosrock.infrastructure.outer.api.oauth.dto.UnlinkKaKaoTarget\nimport org.springframework.cloud.openfeign.FeignClient\nimport org.springframework.http.MediaType\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestHeader\n\n@FeignClient(\n    name = \"KakaoInfoClient\",\n    url = \"https://kapi.kakao.com\",\n    configuration = [KakaoInfoConfig::class],\n)\ninterface KakaoInfoClient {\n\n    @GetMapping(\"/v2/user/me\")\n    fun kakaoUserInfo(@RequestHeader(\"Authorization\") accessToken: String): KakaoInformationResponse\n\n    @PostMapping(path = [\"/v1/user/unlink\"], consumes = [MediaType.APPLICATION_FORM_URLENCODED_VALUE])\n    fun unlinkUser(\n        @RequestHeader(\"Authorization\") adminKey: String,\n        unlinkKaKaoTarget: UnlinkKaKaoTarget,\n    )\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/oauth/client/KakaoOauthClient.kt",
    "content": "package band.gosrock.infrastructure.outer.api.oauth.client\n\nimport band.gosrock.infrastructure.outer.api.oauth.config.KakaoKauthConfig\nimport band.gosrock.infrastructure.outer.api.oauth.dto.KakaoTokenResponse\nimport band.gosrock.infrastructure.outer.api.oauth.dto.OIDCPublicKeysResponse\nimport org.springframework.cache.annotation.Cacheable\nimport org.springframework.cloud.openfeign.FeignClient\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.PostMapping\n\n@FeignClient(\n    name = \"KakaoAuthClient\",\n    url = \"https://kauth.kakao.com\",\n    configuration = [KakaoKauthConfig::class],\n)\ninterface KakaoOauthClient {\n\n    @PostMapping(\"/oauth/token?grant_type=authorization_code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&code={CODE}&client_secret={CLIENT_SECRET}\")\n    fun kakaoAuth(\n        @PathVariable(\"CLIENT_ID\") clientId: String,\n        @PathVariable(\"REDIRECT_URI\") redirectUri: String,\n        @PathVariable(\"CODE\") code: String,\n        @PathVariable(\"CLIENT_SECRET\") clientSecret: String,\n    ): KakaoTokenResponse\n\n    @Cacheable(cacheNames = [\"KakaoOICD\"], cacheManager = \"oidcCacheManager\")\n    @GetMapping(\"/.well-known/jwks.json\")\n    fun getKakaoOIDCOpenKeys(): OIDCPublicKeysResponse\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/oauth/config/KakaoInfoConfig.kt",
    "content": "package band.gosrock.infrastructure.outer.api.oauth.config\n\nimport feign.codec.Encoder\nimport feign.codec.ErrorDecoder\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Import\n\n@Import(KakaoInfoErrorDecoder::class)\nclass KakaoInfoConfig {\n\n    @Bean\n    @ConditionalOnMissingBean(value = [ErrorDecoder::class])\n    fun commonFeignErrorDecoder(): KakaoInfoErrorDecoder = KakaoInfoErrorDecoder()\n\n    @Bean\n    fun formEncoder(): Encoder = feign.form.FormEncoder()\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/oauth/config/KakaoInfoErrorDecoder.kt",
    "content": "package band.gosrock.infrastructure.outer.api.oauth.config\n\nimport band.gosrock.common.exception.OtherServerBadRequestException\nimport band.gosrock.common.exception.OtherServerExpiredTokenException\nimport band.gosrock.common.exception.OtherServerForbiddenException\nimport band.gosrock.common.exception.OtherServerUnauthorizedException\nimport feign.FeignException\nimport feign.Response\nimport feign.codec.ErrorDecoder\n\nclass KakaoInfoErrorDecoder : ErrorDecoder {\n    override fun decode(methodKey: String, response: Response): Exception {\n        if (response.status() >= 400) {\n            when (response.status()) {\n                401 -> throw OtherServerUnauthorizedException.EXCEPTION\n                403 -> throw OtherServerForbiddenException.EXCEPTION\n                419 -> throw OtherServerExpiredTokenException.EXCEPTION\n                else -> throw OtherServerBadRequestException.EXCEPTION\n            }\n        }\n        return FeignException.errorStatus(methodKey, response)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/oauth/config/KakaoKauthConfig.kt",
    "content": "package band.gosrock.infrastructure.outer.api.oauth.config\n\nimport feign.codec.Encoder\nimport feign.codec.ErrorDecoder\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Import\n\n@Import(KauthErrorDecoder::class)\nclass KakaoKauthConfig {\n\n    @Bean\n    @ConditionalOnMissingBean(value = [ErrorDecoder::class])\n    fun commonFeignErrorDecoder(): KauthErrorDecoder = KauthErrorDecoder()\n\n    @Bean\n    fun formEncoder(): Encoder = feign.form.FormEncoder()\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/oauth/config/KauthErrorDecoder.kt",
    "content": "package band.gosrock.infrastructure.outer.api.oauth.config\n\nimport band.gosrock.common.exception.DuDoongDynamicException\nimport band.gosrock.infrastructure.outer.api.oauth.dto.KakaoKauthErrorResponse\nimport band.gosrock.infrastructure.outer.api.oauth.exception.KakaoKauthErrorCode\nimport feign.Response\nimport feign.codec.ErrorDecoder\n\nclass KauthErrorDecoder : ErrorDecoder {\n    override fun decode(methodKey: String, response: Response): Exception {\n        val body = KakaoKauthErrorResponse.from(response)\n        return try {\n            val errorCode = KakaoKauthErrorCode.valueOf(body.errorCode ?: \"\")\n            val errorReason = errorCode.getErrorReason()\n            throw DuDoongDynamicException(errorReason.status, errorReason.code, errorReason.reason)\n        } catch (e: IllegalArgumentException) {\n            val fallback = KakaoKauthErrorCode.KOE_INVALID_REQUEST\n            val errorReason = fallback.getErrorReason()\n            throw DuDoongDynamicException(errorReason.status, errorReason.code, errorReason.reason)\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/oauth/dto/KakaoInformationResponse.kt",
    "content": "package band.gosrock.infrastructure.outer.api.oauth.dto\n\nimport com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy\nimport com.fasterxml.jackson.databind.annotation.JsonNaming\n\n@JsonNaming(SnakeCaseStrategy::class)\nclass KakaoInformationResponse {\n\n    var properties: Properties? = null\n    var id: String? = null\n    var kakaoAccount: KakaoAccount? = null\n\n    @JsonNaming(SnakeCaseStrategy::class)\n    class Properties {\n        var nickname: String? = null\n    }\n\n    @JsonNaming(SnakeCaseStrategy::class)\n    class KakaoAccount {\n        var profile: Profile? = null\n        var email: String? = null\n        var phoneNumber: String? = null\n        var name: String? = null\n\n        @JsonNaming(SnakeCaseStrategy::class)\n        class Profile {\n            var profileImageUrl: String? = null\n        }\n\n        fun getProfileImageUrl(): String? = profile?.profileImageUrl\n    }\n\n    fun getEmail(): String? = kakaoAccount?.email\n\n    fun getPhoneNumber(): String? = kakaoAccount?.phoneNumber\n\n    fun getName(): String? = kakaoAccount?.name ?: properties?.nickname\n\n    fun getProfileUrl(): String? = kakaoAccount?.getProfileImageUrl()\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/oauth/dto/KakaoKauthErrorResponse.kt",
    "content": "package band.gosrock.infrastructure.outer.api.oauth.dto\n\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.PaymentsUnHandleException\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy\nimport com.fasterxml.jackson.databind.annotation.JsonNaming\nimport feign.Response\nimport java.io.IOException\n\n@JsonNaming(SnakeCaseStrategy::class)\nclass KakaoKauthErrorResponse {\n    var error: String? = null\n    var errorCode: String? = null\n    var errorDescription: String? = null\n\n    companion object {\n        @JvmStatic\n        fun from(response: Response): KakaoKauthErrorResponse {\n            return try {\n                response.body().asInputStream().use { bodyIs ->\n                    ObjectMapper().readValue(bodyIs, KakaoKauthErrorResponse::class.java)\n                }\n            } catch (e: IOException) {\n                throw PaymentsUnHandleException.EXCEPTION\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/oauth/dto/KakaoTokenResponse.kt",
    "content": "package band.gosrock.infrastructure.outer.api.oauth.dto\n\nimport com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy\nimport com.fasterxml.jackson.databind.annotation.JsonNaming\n\n@JsonNaming(SnakeCaseStrategy::class)\nclass KakaoTokenResponse {\n    var accessToken: String? = null\n    var refreshToken: String? = null\n    var idToken: String? = null\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/oauth/dto/OIDCPublicKeyDto.kt",
    "content": "package band.gosrock.infrastructure.outer.api.oauth.dto\n\nclass OIDCPublicKeyDto {\n    var kid: String? = null\n    var alg: String? = null\n    var use: String? = null\n    var n: String? = null\n    var e: String? = null\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/oauth/dto/OIDCPublicKeysResponse.kt",
    "content": "package band.gosrock.infrastructure.outer.api.oauth.dto\n\nclass OIDCPublicKeysResponse {\n    var keys: List<OIDCPublicKeyDto>? = null\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/oauth/dto/UnlinkKaKaoTarget.kt",
    "content": "package band.gosrock.infrastructure.outer.api.oauth.dto\n\nclass UnlinkKaKaoTarget(aud: String) {\n\n    @feign.form.FormProperty(\"target_id_type\")\n    val targetIdType: String = \"user_id\"\n\n    @feign.form.FormProperty(\"target_id\")\n    val aud: String = aud\n\n    companion object {\n        @JvmStatic\n        fun from(aud: String): UnlinkKaKaoTarget = UnlinkKaKaoTarget(aud)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/oauth/exception/KakaoKauthErrorCode.kt",
    "content": "package band.gosrock.infrastructure.outer.api.oauth.exception\n\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.consts.DuDoongStatic.BAD_REQUEST\nimport band.gosrock.common.dto.ErrorReason\nimport band.gosrock.common.exception.BaseErrorCode\n\nenum class KakaoKauthErrorCode(\n    private val status: Int,\n    val errorCode: String,\n    private val error: String,\n    private val reason: String,\n) : BaseErrorCode {\n    KOE101(BAD_REQUEST, \"KAKAO_KOE101\", \"invalid_client\", \"잘못된 앱 키 타입을 사용하거나 앱 키에 오타가 있을 경우\"),\n    KOE009(BAD_REQUEST, \"KAKAO_KOE009\", \"misconfigured\", \"등록되지 않은 플랫폼에서 액세스 토큰을 요청 하는 경우\"),\n    KOE010(BAD_REQUEST, \"KAKAO_KOE101\", \"invalid_client\", \"클라이언트 시크릿(Client secret) 기능을 사용하는 앱에서 토큰 요청 시 client_secret 값을 전달하지 않거나 정확하지 않은 값을 전달하는 경우\"),\n    KOE303(BAD_REQUEST, \"KAKAO_KOE303\", \"invalid_grant\", \"인가 코드 요청 시 사용한 redirect_uri와 액세스 토큰 요청 시 사용한 redirect_uri가 다른 경우\"),\n    KOE319(BAD_REQUEST, \"KAKAO_KOE319\", \"invalid_grant\", \"토큰 갱신 요청 시 리프레시 토큰을 전달하지 않는 경우\"),\n    KOE320(BAD_REQUEST, \"KAKAO_KOE320\", \"invalid_grant\", \"동일한 인가 코드를 두 번 이상 사용하거나, 이미 만료된 인가 코드를 사용한 경우, 혹은 인가 코드를 찾을 수 없는 경우\"),\n    KOE322(BAD_REQUEST, \"KAKAO_KOE322\", \"invalid_grant\", \"refresh_token을 찾을 수 없거나 이미 만료된 리프레시 토큰을 사용한 경우\"),\n    KOE_INVALID_REQUEST(BAD_REQUEST, \"KAKAO_KOE_INVALID_REQUEST\", \"invalid_request\", \"잘못된 요청인 경우\"),\n    KOE400(BAD_REQUEST, \"KAKAO_KOE400\", \"invalid_token\", \"ID 토큰 값이 전달되지 않았거나 올바른 형식이 아닌 ID 토큰인 경우\"),\n    KOE401(BAD_REQUEST, \"KAKAO_KOE401\", \"invalid_token\", \"ID 토큰을 발급한 인증 기관(iss)이 카카오 인증 서버\"),\n    KOE402(BAD_REQUEST, \"KAKAO_KOE402\", \"invalid_token\", \"서명이 올바르지 않아 유효한 ID 토큰이 아닌 경우\"),\n    KOE403(BAD_REQUEST, \"KAKAO_KOE403\", \"invalid_token\", \"만료된 ID 토큰인 경우\");\n\n    override fun getErrorReason(): ErrorReason = ErrorReason(status, errorCode, reason)\n\n    @Throws(NoSuchFieldException::class)\n    override fun getExplainError(): String {\n        val field = this::class.java.getField(this.name)\n        val annotation = field.getAnnotation(ExplainError::class.java)\n        return if (annotation != null) annotation.value else reason\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/client/PaymentsCancelClient.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.client\n\nimport band.gosrock.infrastructure.outer.api.tossPayments.config.PaymentsCancelConfig\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.request.CancelPaymentsRequest\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.response.PaymentsResponse\nimport org.springframework.cloud.openfeign.FeignClient\nimport org.springframework.web.bind.annotation.PathVariable\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestBody\nimport org.springframework.web.bind.annotation.RequestHeader\n\n@FeignClient(\n    name = \"PaymentsCancelClient\",\n    url = \"\\${feign.toss.url}\",\n    configuration = [PaymentsCancelConfig::class],\n)\ninterface PaymentsCancelClient {\n    @PostMapping(\"/v1/payments/{paymentKey}/cancel\")\n    fun execute(\n        @RequestHeader(\"Idempotency-Key\") idempotencyKey: String,\n        @PathVariable(\"paymentKey\") paymentKey: String,\n        @RequestBody cancelPaymentsRequest: CancelPaymentsRequest,\n    ): PaymentsResponse\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/client/PaymentsConfirmClient.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.client\n\nimport band.gosrock.infrastructure.outer.api.tossPayments.config.PaymentsConfirmConfig\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.request.ConfirmPaymentsRequest\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.response.PaymentsResponse\nimport org.springframework.cloud.openfeign.FeignClient\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestBody\n\n@FeignClient(\n    name = \"PaymentsConfirmClient\",\n    url = \"https://api.tosspayments.com\",\n    configuration = [PaymentsConfirmConfig::class],\n)\ninterface PaymentsConfirmClient {\n    @PostMapping(\"/v1/payments/confirm\")\n    fun execute(@RequestBody confirmPaymentsRequest: ConfirmPaymentsRequest): PaymentsResponse\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/client/PaymentsCreateClient.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.client\n\nimport band.gosrock.infrastructure.outer.api.tossPayments.config.PaymentsCreateConfig\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.request.CreatePaymentsRequest\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.response.PaymentsResponse\nimport org.springframework.cloud.openfeign.FeignClient\nimport org.springframework.http.MediaType\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestBody\n\n@FeignClient(\n    name = \"PaymentsCreateClient\",\n    url = \"https://api.tosspayments.com\",\n    configuration = [PaymentsCreateConfig::class],\n)\ninterface PaymentsCreateClient {\n    @PostMapping(path = [\"/v1/payments\"], consumes = [MediaType.APPLICATION_JSON_VALUE])\n    fun execute(@RequestBody createPaymentsRequest: CreatePaymentsRequest): PaymentsResponse\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/client/SettlementClient.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.client\n\nimport band.gosrock.infrastructure.outer.api.tossPayments.config.TransactionGetConfig\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.response.SettlementResponse\nimport org.springframework.cloud.openfeign.FeignClient\nimport org.springframework.format.annotation.DateTimeFormat\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.RequestParam\nimport java.time.LocalDate\n\n@FeignClient(\n    name = \"SettlementClient\",\n    url = \"\\${feign.toss.url}\",\n    configuration = [TransactionGetConfig::class],\n)\ninterface SettlementClient {\n    @GetMapping(\"/v1/settlements\")\n    fun execute(\n        @RequestParam(\"startDate\") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate,\n        @RequestParam(\"endDate\") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate,\n        @RequestParam(\"dateType\") dateType: String,\n        @RequestParam(\"page\") page: Int,\n        @RequestParam(\"size\") size: Int,\n    ): List<SettlementResponse>\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/client/TransactionGetClient.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.client\n\nimport band.gosrock.infrastructure.outer.api.tossPayments.config.TransactionGetConfig\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.response.PaymentsResponse\nimport org.springframework.cloud.openfeign.FeignClient\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PathVariable\n\n@FeignClient(\n    name = \"PaymentsGetClient\",\n    url = \"https://api.tosspayments.com\",\n    configuration = [TransactionGetConfig::class],\n)\ninterface TransactionGetClient {\n    @GetMapping(\"/v1/payments/orders/{orderId}\")\n    fun byOrderId(@PathVariable(\"orderId\") orderId: String): PaymentsResponse\n\n    @GetMapping(\"/v1/payments/{paymentKey}\")\n    fun byPaymentKey(@PathVariable(\"paymentKey\") paymentKey: String): PaymentsResponse\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/config/FeignTossConfig.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.config\n\nimport feign.codec.ErrorDecoder\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean\nimport org.springframework.cloud.openfeign.FeignFormatterRegistrar\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Import\nimport org.springframework.format.datetime.standard.DateTimeFormatterRegistrar\n\n@Import(TossHeaderConfig::class)\nclass FeignTossConfig {\n\n    @Bean\n    @ConditionalOnMissingBean(value = [ErrorDecoder::class])\n    fun commonFeignErrorDecoder(): TossErrorDecoder = TossErrorDecoder()\n\n    @Bean\n    fun localDateFeignFormatterRegistrar(): FeignFormatterRegistrar =\n        FeignFormatterRegistrar { formatterRegistry ->\n            DateTimeFormatterRegistrar().apply {\n                setUseIsoFormat(true)\n                registerFormatters(formatterRegistry)\n            }\n        }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/config/PaymentCancelErrorDecoder.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.config\n\nimport band.gosrock.common.exception.DuDoongDynamicException\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.PaymentsCancelErrorCode\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.PaymentsUnHandleException\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.TossPaymentsErrorDto\nimport feign.FeignException\nimport feign.Response\nimport feign.RetryableException\nimport feign.Retryer\nimport feign.codec.ErrorDecoder\nimport org.springframework.context.annotation.Bean\nimport org.springframework.http.HttpStatus\nimport java.util.concurrent.TimeUnit\n\nclass PaymentCancelErrorDecoder : ErrorDecoder {\n\n    @Bean\n    fun retryer(): Retryer.Default = Retryer.Default(PERIOD, MAX_PERIOD, MAX_ATTEMPTS)\n\n    override fun decode(methodKey: String, response: Response): Exception {\n        return try {\n            val exception = FeignException.errorStatus(methodKey, response)\n            val status = response.status()\n            if (HttpStatus.valueOf(status).is5xxServerError) {\n                throw RetryableException(\n                    status,\n                    exception.message,\n                    response.request().httpMethod(),\n                    exception,\n                    null as Long?,\n                    response.request(),\n                )\n            }\n            val body = TossPaymentsErrorDto.from(response)\n            val errorCode = PaymentsCancelErrorCode.valueOf(body.code ?: \"\")\n            val errorReason = errorCode.getErrorReason()\n            throw DuDoongDynamicException(errorReason.status, errorReason.code, errorReason.reason)\n        } catch (e: IllegalArgumentException) {\n            throw PaymentsUnHandleException.EXCEPTION\n        }\n    }\n\n    companion object {\n        private const val PERIOD = 500L\n        private val MAX_PERIOD = TimeUnit.SECONDS.toMillis(3L)\n        private const val MAX_ATTEMPTS = 3\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/config/PaymentConfirmErrorDecoder.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.config\n\nimport band.gosrock.common.exception.DuDoongDynamicException\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.PaymentsConfirmErrorCode\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.PaymentsUnHandleException\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.TossPaymentsErrorDto\nimport feign.Response\nimport feign.codec.ErrorDecoder\n\nclass PaymentConfirmErrorDecoder : ErrorDecoder {\n    override fun decode(methodKey: String, response: Response): Exception {\n        val body = TossPaymentsErrorDto.from(response)\n        return try {\n            val errorCode = PaymentsConfirmErrorCode.valueOf(body.code ?: \"\")\n            val errorReason = errorCode.getErrorReason()\n            throw DuDoongDynamicException(errorReason.status, errorReason.code, errorReason.reason)\n        } catch (e: IllegalArgumentException) {\n            throw PaymentsUnHandleException.EXCEPTION\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/config/PaymentCreateErrorDecoder.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.config\n\nimport band.gosrock.common.exception.DuDoongDynamicException\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.PaymentsCreateErrorCode\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.PaymentsUnHandleException\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.TossPaymentsErrorDto\nimport feign.Response\nimport feign.codec.ErrorDecoder\n\nclass PaymentCreateErrorDecoder : ErrorDecoder {\n    override fun decode(methodKey: String, response: Response): Exception {\n        val body = TossPaymentsErrorDto.from(response)\n        return try {\n            val errorCode = PaymentsCreateErrorCode.valueOf(body.code ?: \"\")\n            val errorReason = errorCode.getErrorReason()\n            throw DuDoongDynamicException(errorReason.status, errorReason.code, errorReason.reason)\n        } catch (e: IllegalArgumentException) {\n            throw PaymentsUnHandleException.EXCEPTION\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/config/PaymentsCancelConfig.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.config\n\nimport feign.codec.ErrorDecoder\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean\nimport org.springframework.cloud.openfeign.FeignFormatterRegistrar\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Import\nimport org.springframework.format.datetime.standard.DateTimeFormatterRegistrar\n\n@Import(TossHeaderConfig::class, PaymentCancelErrorDecoder::class)\nclass PaymentsCancelConfig {\n\n    @Bean\n    @ConditionalOnMissingBean(value = [ErrorDecoder::class])\n    fun commonFeignErrorDecoder(): PaymentCancelErrorDecoder = PaymentCancelErrorDecoder()\n\n    @Bean\n    fun localDateFeignFormatterRegistrar(): FeignFormatterRegistrar =\n        FeignFormatterRegistrar { formatterRegistry ->\n            DateTimeFormatterRegistrar().apply {\n                setUseIsoFormat(true)\n                registerFormatters(formatterRegistry)\n            }\n        }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/config/PaymentsConfirmConfig.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.config\n\nimport feign.codec.ErrorDecoder\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean\nimport org.springframework.cloud.openfeign.FeignFormatterRegistrar\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Import\nimport org.springframework.format.datetime.standard.DateTimeFormatterRegistrar\n\n@Import(TossHeaderConfig::class, PaymentConfirmErrorDecoder::class)\nclass PaymentsConfirmConfig {\n\n    @Bean\n    @ConditionalOnMissingBean(value = [ErrorDecoder::class])\n    fun commonFeignErrorDecoder(): PaymentConfirmErrorDecoder = PaymentConfirmErrorDecoder()\n\n    @Bean\n    fun localDateFeignFormatterRegistrar(): FeignFormatterRegistrar =\n        FeignFormatterRegistrar { formatterRegistry ->\n            DateTimeFormatterRegistrar().apply {\n                setUseIsoFormat(true)\n                registerFormatters(formatterRegistry)\n            }\n        }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/config/PaymentsCreateConfig.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.config\n\nimport feign.codec.ErrorDecoder\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean\nimport org.springframework.cloud.openfeign.FeignFormatterRegistrar\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Import\nimport org.springframework.format.datetime.standard.DateTimeFormatterRegistrar\n\n@Import(TossHeaderConfig::class, PaymentCreateErrorDecoder::class)\nclass PaymentsCreateConfig {\n\n    @Bean\n    @ConditionalOnMissingBean(value = [ErrorDecoder::class])\n    fun commonFeignErrorDecoder(): PaymentCreateErrorDecoder = PaymentCreateErrorDecoder()\n\n    @Bean\n    fun localDateFeignFormatterRegistrar(): FeignFormatterRegistrar =\n        FeignFormatterRegistrar { formatterRegistry ->\n            DateTimeFormatterRegistrar().apply {\n                setUseIsoFormat(true)\n                registerFormatters(formatterRegistry)\n            }\n        }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/config/TossErrorDecoder.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.config\n\nimport band.gosrock.common.exception.OtherServerBadRequestException\nimport band.gosrock.common.exception.OtherServerExpiredTokenException\nimport band.gosrock.common.exception.OtherServerForbiddenException\nimport band.gosrock.common.exception.OtherServerUnauthorizedException\nimport feign.FeignException\nimport feign.Response\nimport feign.codec.ErrorDecoder\n\nclass TossErrorDecoder : ErrorDecoder {\n    override fun decode(methodKey: String, response: Response): Exception {\n        if (response.status() >= 400) {\n            when (response.status()) {\n                401 -> throw OtherServerUnauthorizedException.EXCEPTION\n                403 -> throw OtherServerForbiddenException.EXCEPTION\n                404 -> throw OtherServerForbiddenException.EXCEPTION\n                419 -> throw OtherServerExpiredTokenException.EXCEPTION\n                else -> throw OtherServerBadRequestException.EXCEPTION\n            }\n        }\n        return FeignException.errorStatus(methodKey, response)\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/config/TossHeaderConfig.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.config\n\nimport band.gosrock.common.properties.TossPaymentsProperties\nimport feign.auth.BasicAuthRequestInterceptor\nimport org.springframework.context.annotation.Bean\n\nclass TossHeaderConfig(private val tossPaymentsProperties: TossPaymentsProperties) {\n\n    @Bean\n    fun basicAuthRequestInterceptor(): BasicAuthRequestInterceptor =\n        BasicAuthRequestInterceptor(tossPaymentsProperties.secretKey, \"\")\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/config/TransactionGetConfig.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.config\n\nimport feign.codec.ErrorDecoder\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean\nimport org.springframework.cloud.openfeign.FeignFormatterRegistrar\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Import\nimport org.springframework.format.datetime.standard.DateTimeFormatterRegistrar\n\n@Import(TossHeaderConfig::class, TransactionGetErrorDecoder::class)\nclass TransactionGetConfig {\n\n    @Bean\n    @ConditionalOnMissingBean(value = [ErrorDecoder::class])\n    fun commonFeignErrorDecoder(): TransactionGetErrorDecoder = TransactionGetErrorDecoder()\n\n    @Bean\n    fun localDateFeignFormatterRegistrar(): FeignFormatterRegistrar =\n        FeignFormatterRegistrar { formatterRegistry ->\n            DateTimeFormatterRegistrar().apply {\n                setUseIsoFormat(true)\n                registerFormatters(formatterRegistry)\n            }\n        }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/config/TransactionGetErrorDecoder.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.config\n\nimport band.gosrock.common.exception.DuDoongDynamicException\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.PaymentsUnHandleException\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.TossPaymentsErrorDto\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.TransactionGetErrorCode\nimport feign.Response\nimport feign.codec.ErrorDecoder\n\nclass TransactionGetErrorDecoder : ErrorDecoder {\n    override fun decode(methodKey: String, response: Response): Exception {\n        val body = TossPaymentsErrorDto.from(response)\n        return try {\n            val errorCode = TransactionGetErrorCode.valueOf(body.code ?: \"\")\n            val errorReason = errorCode.getErrorReason()\n            throw DuDoongDynamicException(errorReason.status, errorReason.code, errorReason.reason)\n        } catch (e: IllegalArgumentException) {\n            throw PaymentsUnHandleException.EXCEPTION\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/request/CancelPaymentsRequest.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.request\n\n// 부분 취소 안합니다. 전체 금액 취소입니다.\ndata class CancelPaymentsRequest(val cancelReason: String? = null)\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/request/ConfirmPaymentsRequest.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.request\n\ndata class ConfirmPaymentsRequest(\n    val paymentKey: String? = null,\n    val orderId: String? = null,\n    val amount: Long? = null,\n)\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/request/CreatePaymentsRequest.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.request\n\ndata class CreatePaymentsRequest(\n    val method: String? = null,\n    val amount: Long? = null,\n    val orderId: String? = null,\n    val orderName: String? = null,\n    val successUrl: String? = null,\n    val failUrl: String? = null,\n)\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/response/CardAcquireStatus.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.response\n\nenum class CardAcquireStatus(val value: String) {\n    READY(\"READY\"),\n    REQUESTED(\"REQUESTED\"),\n    COMPLETED(\"COMPLETED\"),\n    CANCEL_REQUESTED(\"CANCEL_REQUESTED\"),\n    CANCELED(\"CANCELED\"),\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/response/CardCode.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.response\n\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.PaymentsEnumNotMatchException\nimport com.fasterxml.jackson.annotation.JsonCreator\n\nenum class CardCode(val code: String, val kr: String, val en: String, val cardCompanyName: String) {\n    IBK_BC(\"3K\", \"기업비씨\", \"IBK_BC\", \"기업 비씨\"),\n    GWANGJUBANK(\"46\", \"광주\", \"GWANGJUBANK\", \"광주은행\"),\n    LOTTE(\"71\", \"롯데\", \"LOTTE\", \"롯데카드\"),\n    KDBBANK(\"30\", \"산업\", \"KDBBANK\", \"KDB산업은행\"),\n    BC(\"31\", \"\", \"BC\", \"비씨카드\"),\n    SAMSUNG(\"51\", \"삼성\", \"SAMSUNG\", \"삼성카드\"),\n    SAEMAUL(\"38\", \"새마을\", \"SAEMAUL\", \"새마을금고\"),\n    SHINHAN(\"41\", \"신한\", \"SHINHAN\", \"신한카드\"),\n    SHINHYEOP(\"62\", \"신협\", \"SHINHYEOP\", \"신협\"),\n    CITI(\"36\", \"씨티\", \"CITI\", \"씨티카드\"),\n    WOORI(\"33\", \"우리\", \"WOORI\", \"우리카드\"),\n    WOORI_ACQUIRE(\"W1\", \"우리\", \"WOORI\", \"우리카드\"),\n    POST(\"37\", \"우체국\", \"POST\", \"우체국예금보험\"),\n    SAVINGBANK(\"39\", \"저축\", \"SAVINGBANK\", \"저축은행중앙회\"),\n    JEONBUKBANK(\"35\", \"전북\", \"JEONBUKBANK\", \"전북은행\"),\n    JEJUBANK(\"42\", \"제주\", \"JEJUBANK\", \"제주은행\"),\n    KAKAOBANK(\"15\", \"카카오뱅크\", \"KAKAOBANK\", \"카카오뱅크\"),\n    KBANK(\"3A\", \"케이뱅크\", \"KBANK\", \"케이뱅크\"),\n    TOSSBANK(\"24\", \"토스뱅크\", \"TOSSBANK\", \"토스뱅크\"),\n    HANA(\"21\", \"하나\", \"HANA\", \"하나카드\"),\n    HYUNDAI(\"61\", \"현대\", \"HYUNDAI\", \"현대카드\"),\n    KOOKMIN(\"11\", \"국민\", \"KOOKMIN\", \"KB국민카드\"),\n    NONGHYEOP(\"91\", \"농협\", \"NONGHYEOP\", \"NH농협카드\"),\n    SUHYEOP(\"34\", \"수협\", \"SUHYEOP\", \"Sh수협은행\"),\n    DINERS(\"6D\", \"다이너스\", \"DINERS\", \"다이너스 클럽\"),\n    DISCOVER(\"6I\", \"디스커버\", \"DISCOVER\", \"디스커버\"),\n    MASTER(\"4M\", \"마스터\", \"MASTER\", \"마스터카드\"),\n    UNIONPAY(\"3C\", \"유니온페이\", \"UNIONPAY\", \"유니온페이\"),\n    AMEX(\"7A\", \"\", \"AMEX\", \"아메리칸 익스프레스\"),\n    JCB(\"4J\", \"\", \"JCB\", \"JCB\"),\n    VISA(\"4V\", \"비자\", \"VISA\", \"VISA\");\n\n    companion object {\n        @JsonCreator\n        @JvmStatic\n        fun findValue(code: String): CardCode =\n            values().firstOrNull { it.code == code }\n                ?: throw PaymentsEnumNotMatchException.EXCEPTION\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/response/EasyPayCode.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.response\n\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.PaymentsEnumNotMatchException\nimport com.fasterxml.jackson.annotation.JsonCreator\n\nenum class EasyPayCode(val kr: String, val en: String) {\n    TOSSPAY(\"토스페이\", \"TOSSPAY\"),\n    NAVERPAY(\"네이버페이\", \"NAVERPAY\"),\n    SAMSUNGPAY(\"삼성페이\", \"SAMSUNGPAY\"),\n    LPAY(\"엘페이\", \"LPAY\"),\n    KAKAOPAY(\"카카오페이\", \"KAKAOPAY\"),\n    PAYCO(\"페이코\", \"PAYCO\"),\n    LGPAY(\"LG페이\", \"LGPAY\"),\n    SSG(\"SSG페이\", \"SSG\");\n\n    companion object {\n        @JsonCreator\n        @JvmStatic\n        fun findValue(code: String): EasyPayCode =\n            values().firstOrNull { it.kr == code }\n                ?: throw PaymentsEnumNotMatchException.EXCEPTION\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/response/FeeCode.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.response\n\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.PaymentsEnumNotMatchException\nimport com.fasterxml.jackson.annotation.JsonCreator\n\nenum class FeeCode(val code: String, val kr: String) {\n    BASE(\"BASE\", \"기본 수수료\"),\n    INSTALLMENT_DISCOUNT(\"INSTALLMENT_DISCOUNT\", \"PG사 부담 수수료\"),\n    INSTALLMENT(\"INSTALLMENT\", \"상점 부담 무이자 할부 수수료\"),\n    POINT_SAVING(\"POINT_SAVING\", \"카드사 포인트 적립 수수료\"),\n    ETC(\"ETC\", \"기본 수수료\");\n\n    companion object {\n        @JsonCreator\n        @JvmStatic\n        fun findValue(code: String): FeeCode =\n            values().firstOrNull { it.code == code }\n                ?: throw PaymentsEnumNotMatchException.EXCEPTION\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/response/PaymentCheckout.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.response\n\nclass PaymentCheckout {\n    var url: String? = null\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/response/PaymentEasyPay.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.response\n\nclass PaymentEasyPay {\n    var provider: EasyPayCode? = null\n    var amount: Long? = null\n    var discountAmount: Long? = null\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/response/PaymentReceipt.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.response\n\nclass PaymentReceipt {\n    var url: String? = null\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/response/PaymentStatus.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.response\n\nenum class PaymentStatus(val value: String) {\n    READY(\"READY\"),\n    IN_PROGRESS(\"IN_PROGRESS\"),\n    WAITING_FOR_DEPOSIT(\"WAITING_FOR_DEPOSIT\"),\n    DONE(\"DONE\"),\n    CANCELED(\"CANCELED\"),\n    PARTIAL_CANCELED(\"PARTIAL_CANCELED\"),\n    ABORTED(\"ABORTED\"),\n    EXPIRED(\"EXPIRED\"),\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/response/PaymentsCancels.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.response\n\nimport java.time.ZonedDateTime\n\nclass PaymentsCancels {\n    var cancelAmount: Long? = null\n    var cancelReason: String? = null\n    var taxFreeAmount: Long? = null\n    var taxExceptionAmount: Long? = null\n    var refundableAmount: Long? = null\n    var easyPayDiscountAmount: Long? = null\n    var canceledAt: ZonedDateTime? = null\n    var transactionKey: String? = null\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/response/PaymentsCard.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.response\n\nclass PaymentsCard {\n    var amount: Long? = null\n    var issuerCode: CardCode? = null\n    var acquirerCode: CardCode? = null\n    var number: String? = null\n    var installmentPlanMonths: Long? = null\n    var approveNo: String? = null\n    var useCardPoint: Boolean? = null\n    var cardType: String? = null\n    var ownerType: String? = null\n    var acquireStatus: CardAcquireStatus? = null\n    var isInterestFree: Boolean? = null\n    var interestPayer: String? = null\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/response/PaymentsCardPromotion.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.response\n\nclass PaymentsCardPromotion {\n    var amount: Long? = null\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/response/PaymentsCashReceipt.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.response\n\nclass PaymentsCashReceipt {\n    var receiptKey: String? = null\n    var type: String? = null\n    var amount: Long? = null\n    var taxFreeAmount: Long? = null\n    var issueNumber: String? = null\n    var receiptUrl: String? = null\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/response/PaymentsFailure.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.response\n\nclass PaymentsFailure {\n    var code: String? = null\n    var message: String? = null\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/response/PaymentsResponse.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.response\n\nimport java.time.ZonedDateTime\n\nclass PaymentsResponse {\n    var version: String? = null\n    var paymentKey: String? = null\n    var type: String? = null\n    var orderId: String? = null\n    var orderName: String? = null\n    var mId: String? = null\n    var currency: String? = null\n    var method: TossPaymentMethod? = null\n    var totalAmount: Long? = null\n    var balanceAmount: Long? = null\n    var status: PaymentStatus? = null\n    var requestedAt: ZonedDateTime? = null\n    var approvedAt: ZonedDateTime? = null\n    var useEscrow: Boolean? = null\n    var lastTransactionKey: String? = null\n    var suppliedAmount: Long? = null\n    var vat: Long? = null\n    var cultureExpense: Boolean? = null\n    var taxFreeAmount: Long? = null\n    var taxExemptionAmount: Long? = null\n    var cancels: List<PaymentsCancels>? = null\n    var isPartialCancelable: Boolean? = null\n    var receipt: PaymentReceipt? = null\n    var checkout: PaymentCheckout? = null\n    var easyPay: PaymentEasyPay? = null\n    var card: PaymentsCard? = null\n    var country: String? = null\n    var failure: PaymentsFailure? = null\n    var cashReceipt: PaymentsCashReceipt? = null\n    var discount: PaymentsCardPromotion? = null\n\n    fun getProviderName(): String {\n        return when (method) {\n            TossPaymentMethod.CARD -> card?.issuerCode?.kr ?: \"\"\n            TossPaymentMethod.EASYPAY -> easyPay?.provider?.kr ?: \"\"\n            else -> \"\"\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/response/SettlementFeeDto.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.response\n\nclass SettlementFeeDto {\n    var type: FeeCode? = null\n    var fee: Long? = null\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/response/SettlementResponse.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.response\n\nimport java.time.LocalDate\nimport java.time.ZonedDateTime\n\nclass SettlementResponse {\n    var paymentKey: String? = null\n    var transactionKey: String? = null\n    var orderId: String? = null\n    var orderName: String? = null\n    var mId: String? = null\n    var currency: String? = null\n    var method: TossPaymentMethod? = null\n    var amount: Long? = null\n    var supplyAmount: Long? = null\n    var vat: Long? = null\n    var payOutAmount: Long? = null\n    var interestFee: Long? = null\n    var approvedAt: ZonedDateTime? = null\n    var soldDate: LocalDate? = null\n    var paidOutDate: LocalDate? = null\n    var fees: List<SettlementFeeDto>? = null\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/dto/response/TossPaymentMethod.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.dto.response\n\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.PaymentsEnumNotMatchException\nimport com.fasterxml.jackson.annotation.JsonCreator\n\nenum class TossPaymentMethod(val kr: String) {\n    CARD(\"카드\"),\n    VIRTUAL_ACCOUNT(\"가상계좌\"),\n    EASYPAY(\"간편결제\"),\n    MOBILE_PAY(\"휴대폰\"),\n    BANK_TRANSFER(\"계좌이체\"),\n    GIFT_CARD(\"상품권\");\n\n    companion object {\n        @JsonCreator\n        @JvmStatic\n        fun findValue(code: String): TossPaymentMethod =\n            values().firstOrNull { it.kr == code }\n                ?: throw PaymentsEnumNotMatchException.EXCEPTION\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/exception/PaymentsCancelErrorCode.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.exception\n\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.dto.ErrorReason\nimport band.gosrock.common.exception.BaseErrorCode\nimport org.springframework.http.HttpStatus.BAD_REQUEST\nimport org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR\n\nenum class PaymentsCancelErrorCode(\n    private val status: Int,\n    private val code: String,\n    private val reason: String,\n) : BaseErrorCode {\n    ALREADY_CANCELED_PAYMENT(INTERNAL_SERVER_ERROR.value(), \"PAYMENTS_GET_ALREADY_CANCELED_PAYMENT\", \"이미 취소된 결제 입니다.\"),\n    INVALID_REFUND_ACCOUNT_INFO(BAD_REQUEST.value(), \"PAYMENTS_GET_INVALID_REFUND_ACCOUNT_INFO\", \"환불 계좌번호와 예금주명이 일치하지 않습니다.\"),\n    EXCEED_CANCEL_AMOUNT_DISCOUNT_AMOUNT(BAD_REQUEST.value(), \"PAYMENTS_GET_EXCEED_CANCEL_AMOUNT_DISCOUNT_AMOUNT\", \"즉시할인금액보다 적은 금액은 부분취소가 불가능합니다.\"),\n    INVALID_REQUEST(BAD_REQUEST.value(), \"PAYMENTS_GET_NOT_FOUND\", \"잘못된 요청입니다.\"),\n    INVALID_REFUND_ACCOUNT_NUMBER(BAD_REQUEST.value(), \"PAYMENTS_GET_INVALID_REFUND_ACCOUNT_NUMBER\", \"잘못된 환불 계좌번호입니다.\"),\n    INVALID_BANK(BAD_REQUEST.value(), \"PAYMENTS_GET_INVALID_BANK\", \"잘못된 요청입니다.\"),\n    NOT_MATCHES_REFUNDABLE_AMOUNT(BAD_REQUEST.value(), \"PAYMENTS_GET_NOT_MATCHES_REFUNDABLE_AMOUNT\", \"잔액 결과가 일치하지 않습니다.\"),\n    PROVIDER_ERROR(BAD_REQUEST.value(), \"PAYMENTS_GET_PROVIDER_ERROR\", \"일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요.\"),\n    UNAUTHORIZED_KEY(BAD_REQUEST.value(), \"PAYMENTS_GET_UNAUTHORIZED_KEY\", \"인증되지 않은 시크릿 키 혹은 클라이언트 키 입니다.\"),\n    NOT_CANCELABLE_AMOUNT(BAD_REQUEST.value(), \"PAYMENTS_GET_NOT_CANCELABLE_AMOUNT\", \"취소 할 수 없는 금액 입니다.\"),\n    FORBIDDEN_CONSECUTIVE_REQUEST(BAD_REQUEST.value(), \"PAYMENTS_GET_FORBIDDEN_CONSECUTIVE_REQUEST\", \"반복적인 요청은 허용되지 않습니다. 잠시 후 다시 시도해주세요.\"),\n    FORBIDDEN_REQUEST(BAD_REQUEST.value(), \"PAYMENTS_GET_FORBIDDEN_REQUEST\", \"허용되지 않은 요청입니다.\"),\n    NOT_CANCELABLE_PAYMENT(BAD_REQUEST.value(), \"PAYMENTS_GET_NOT_CANCELABLE_PAYMENT\", \"취소 할 수 없는 결제 입니다.\"),\n    EXCEED_MAX_REFUND_DUE(BAD_REQUEST.value(), \"PAYMENTS_GET_EXCEED_MAX_REFUND_DUE\", \"환불 가능한 기간이 지났습니다.\"),\n    NOT_ALLOWED_PARTIAL_REFUND_WAITING_DEPOSIT(BAD_REQUEST.value(), \"PAYMENTS_GET_NOT_FOUND\", \"입금 대기중인 결제는 부분 환불이 불가합니다\"),\n    NOT_ALLOWED_PARTIAL_REFUND(BAD_REQUEST.value(), \"PAYMENTS_GET_NOT_ALLOWED_PARTIAL_REFUND\", \"에스크로 주문이나 체크 카드 결제는 부분 환불이 되지 않습니다.\"),\n    NOT_AVAILABLE_BANK(BAD_REQUEST.value(), \"PAYMENTS_GET_NOT_AVAILABLE_BANK\", \"은행 서비스 시간이 아닙니다.\"),\n    NOT_FOUND_PAYMENT(BAD_REQUEST.value(), \"PAYMENTS_GET_NOT_FOUND_PAYMENT\", \"존재하지 않는 결제 정보 입니다.\"),\n    FAILED_INTERNAL_SYSTEM_PROCESSING(BAD_REQUEST.value(), \"PAYMENTS_GET_FAILED_INTERNAL_SYSTEM_PROCESSING\", \"내부 시스템 처리 작업이 실패했습니다. 잠시 후 다시 시도해주세요.\"),\n    FAILED_REFUND_PROCESS(BAD_REQUEST.value(), \"PAYMENTS_GET_FAILED_REFUND_PROCESS\", \"은행 응답시간 지연이나 일시적인 오류로 환불요청에 실패했습니다.\"),\n    FAILED_METHOD_HANDLING_CANCEL(BAD_REQUEST.value(), \"PAYMENTS_GET_FAILED_METHOD_HANDLING_CANCEL\", \"취소 중 결제 시 사용한 결제 수단 처리과정에서 일시적인 오류가 발생했습니다.\"),\n    FAILED_PARTIAL_REFUND(BAD_REQUEST.value(), \"PAYMENTS_GET_FAILED_PARTIAL_REFUND\", \"은행 점검, 해약 계좌 등의 사유로 부분 환불이 실패했습니다.\"),\n    COMMON_ERROR(BAD_REQUEST.value(), \"PAYMENTS_GET_COMMON_ERROR\", \"일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요.\"),\n    FAILED_PAYMENT_INTERNAL_SYSTEM_PROCESSING(BAD_REQUEST.value(), \"PAYMENTS_GET_FAILED_PAYMENT_INTERNAL_SYSTEM_PROCESSING\", \"결제가 완료되지 않았어요. 다시 시도해주세요.\");\n\n    override fun getErrorReason(): ErrorReason = ErrorReason(status, code, reason)\n\n    @Throws(NoSuchFieldException::class)\n    override fun getExplainError(): String {\n        val field = this::class.java.getField(this.name)\n        val annotation = field.getAnnotation(ExplainError::class.java)\n        return if (annotation != null) annotation.value else reason\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/exception/PaymentsConfirmErrorCode.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.exception\n\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.dto.ErrorReason\nimport band.gosrock.common.exception.BaseErrorCode\nimport org.springframework.http.HttpStatus.BAD_REQUEST\n\nenum class PaymentsConfirmErrorCode(\n    private val status: Int,\n    private val code: String,\n    private val reason: String,\n) : BaseErrorCode {\n    ALREADY_PROCESSED_PAYMENT(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_ALREADY_PROCESSED_PAYMENT\", \"이미 처리된 결제 입니다.\"),\n    PROVIDER_ERROR(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_PROVIDER_ERROR\", \"일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요.\"),\n    EXCEED_MAX_CARD_INSTALLMENT_PLAN(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_EXCEED_MAX_CARD_INSTALLMENT_PLAN\", \"설정 가능한 최대 할부 개월 수를 초과했습니다.\"),\n    INVALID_REQUEST(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_INVALID_REQUEST\", \"잘못된 요청입니다.\"),\n    INVALID_API_KEY(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_INVALID_API_KEY\", \"잘못된 시크릿키 연동 정보 입니다.\"),\n    INVALID_REJECT_CARD(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_INVALID_API_KEY\", \"카드 사용이 거절되었습니다. 카드사 문의가 필요합니다.\"),\n    BELOW_MINIMUM_AMOUNT(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_BELOW_MINIMUM_AMOUNT\", \"신용카드는 결제금액이 100원 이상, 계좌는 200원이상부터 결제가 가능합니다.\"),\n    INVALID_CARD_EXPIRATION(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_INVALID_CARD_EXPIRATION\", \"카드 정보를 다시 확인해주세요. (유효기간)\"),\n    EXCEED_MAX_DAILY_PAYMENT_COUNT(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_EXCEED_MAX_DAILY_PAYMENT_COUNT\", \"하루 결제 가능 횟수를 초과했습니다.\"),\n    NOT_SUPPORTED_INSTALLMENT_PLAN_CARD_OR_MERCHANT(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_NOT_SUPPORTED_INSTALLMENT_PLAN_CARD_OR_MERCHANT\", \"할부가 지원되지 않는 카드 또는 가맹점 입니다.\"),\n    INVALID_CARD_INSTALLMENT_PLAN(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_INVALID_CARD_INSTALLMENT_PLAN\", \"할부 개월 정보가 잘못되었습니다.\"),\n    NOT_SUPPORTED_MONTHLY_INSTALLMENT_PLAN(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_NOT_SUPPORTED_MONTHLY_INSTALLMENT_PLAN\", \"할부가 지원되지 않는 카드입니다.\"),\n    EXCEED_MAX_PAYMENT_AMOUNT(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_EXCEED_MAX_PAYMENT_AMOUNT\", \"하루 결제 가능 금액을 초과했습니다.\"),\n    NOT_FOUND_TERMINAL_ID(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_NOT_FOUND_TERMINAL_ID\", \"단말기번호(Terminal Id)가 없습니다. 토스페이먼츠로 문의 바랍니다.\"),\n    INVALID_AUTHORIZE_AUTH(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_INVALID_AUTHORIZE_AUTH\", \"유효하지 않은 인증 방식입니다.\"),\n    INVALID_CARD_LOST_OR_STOLEN(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_INVALID_CARD_LOST_OR_STOLEN\", \"분실 혹은 도난 카드입니다.\"),\n    INVALID_CARD_NUMBER(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_INVALID_CARD_NUMBER\", \"카드번호를 다시 확인해주세요.\"),\n    INVALID_UNREGISTERED_SUBMALL(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_INVALID_UNREGISTERED_SUBMALL\", \"등록되지 않은 서브몰입니다. 서브몰이 없는 가맹점이라면 안심클릭이나 ISP 결제가 필요합니다.\"),\n    NOT_REGISTERED_BUSINESS(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_NOT_REGISTERED_BUSINESS\", \"등록되지 않은 사업자 번호입니다.\"),\n    EXCEED_MAX_ONE_DAY_WITHDRAW_AMOUNT(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_EXCEED_MAX_ONE_DAY_WITHDRAW_AMOUNT\", \"1일 출금 한도를 초과했습니다.\"),\n    EXCEED_MAX_ONE_TIME_WITHDRAW_AMOUNT(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_EXCEED_MAX_ONE_TIME_WITHDRAW_AMOUNT\", \"1회 출금 한도를 초과했습니다.\"),\n    CARD_PROCESSING_ERROR(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_CARD_PROCESSING_ERROR\", \"카드사에서 오류가 발생했습니다.\"),\n    EXCEED_MAX_AMOUNT(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_EXCEED_MAX_AMOUNT\", \"거래금액 한도를 초과했습니다.\"),\n    INVALID_ACCOUNT_INFO_RE_REGISTER(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_INVALID_ACCOUNT_INFO_RE_REGISTER\", \"유효하지 않은 계좌입니다. 계좌 재등록 후 시도해주세요.\"),\n    UNAUTHORIZED_KEY(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_UNAUTHORIZED_KEY\", \"인증되지 않은 시크릿 키 혹은 클라이언트 키 입니다.\"),\n    REJECT_ACCOUNT_PAYMENT(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_INVALID_ACCOUNT_INFO_RE_REGISTER\", \"잔액부족으로 결제에 실패했습니다.\"),\n    REJECT_CARD_PAYMENT(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_REJECT_CARD_PAYMENT\", \"한도초과 혹은 잔액부족으로 결제에 실패했습니다.\"),\n    REJECT_CARD_COMPANY(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_REJECT_CARD_COMPANY\", \"결제 승인이 거절되었습니다.\"),\n    FORBIDDEN_REQUEST(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_FORBIDDEN_REQUEST\", \"허용되지 않은 요청입니다.\"),\n    REJECT_TOSSPAY_INVALID_ACCOUNT(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_REJECT_TOSSPAY_INVALID_ACCOUNT\", \"선택하신 출금 계좌가 출금이체 등록이 되어 있지 않아요. 계좌를 다시 등록해 주세요.\"),\n    EXCEED_MAX_AUTH_COUNT(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_EXCEED_MAX_AUTH_COUNT\", \"최대 인증 횟수를 초과했습니다. 카드사로 문의해주세요.\"),\n    EXCEED_MAX_ONE_DAY_AMOUNT(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_EXCEED_MAX_ONE_DAY_AMOUNT\", \"일일 한도를 초과했습니다.\"),\n    NOT_AVAILABLE_BANK(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_NOT_AVAILABLE_BANK\", \"은행 서비스 시간이 아닙니다.\"),\n    INVALID_PASSWORD(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_INVALID_PASSWORD\", \"결제 비밀번호가 일치하지 않습니다.\"),\n    NOT_FOUND_PAYMENT(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_NOT_FOUND_PAYMENT\", \"존재하지 않는 결제 정보 입니다.\"),\n    NOT_FOUND_PAYMENT_SESSION(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_NOT_FOUND_PAYMENT_SESSION\", \"결제 시간이 만료되어 결제 진행 데이터가 존재하지 않습니다.\"),\n    FAILED_PAYMENT_INTERNAL_SYSTEM_PROCESSING(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_FAILED_PAYMENT_INTERNAL_SYSTEM_PROCESSING\", \"결제가 완료되지 않았어요. 다시 시도해주세요.\"),\n    FAILED_INTERNAL_SYSTEM_PROCESSING(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_FAILED_INTERNAL_SYSTEM_PROCESSING\", \"내부 시스템 처리 작업이 실패했습니다. 잠시 후 다시 시도해주세요.\"),\n    UNKNOWN_PAYMENT_ERROR(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_UNKNOWN_PAYMENT_ERROR\", \"결제에 실패했어요. 같은 문제가 반복된다면 은행이나 카드사로 문의해주세요.\"),\n    RESTRICTED_TRANSFER_ACCOUNT(BAD_REQUEST.value(), \"PAYMENTS_CONFIRM_RESTRICTED_TRANSFER_ACCOUNT\", \"계좌는 등록 후 12시간 뒤부터 결제할 수 있습니다. 관련 정책은 해당 은행으로 문의해주세요.\");\n\n    override fun getErrorReason(): ErrorReason = ErrorReason(status, code, reason)\n\n    @Throws(NoSuchFieldException::class)\n    override fun getExplainError(): String {\n        val field = this::class.java.getField(this.name)\n        val annotation = field.getAnnotation(ExplainError::class.java)\n        return if (annotation != null) annotation.value else reason\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/exception/PaymentsCreateErrorCode.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.exception\n\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.dto.ErrorReason\nimport band.gosrock.common.exception.BaseErrorCode\nimport org.springframework.http.HttpStatus.BAD_REQUEST\n\nenum class PaymentsCreateErrorCode(\n    private val status: Int,\n    private val code: String,\n    private val reason: String,\n) : BaseErrorCode {\n    NOT_SUPPORTED_METHOD(BAD_REQUEST.value(), \"PAYMENTS_CREATE_NOT_SUPPORTED_METHOD\", \"지원되지 않는 결제 수단입니다.\"),\n    INVALID_SUCCESS_URL(BAD_REQUEST.value(), \"PAYMENTS_CREATE_INVALID_SUCCESS_URL\", \"`successUrl`은 필수 파라미터입니다.\"),\n    DUPLICATED_ORDER_ID(BAD_REQUEST.value(), \"PAYMENTS_CREATE_DUPLICATED_ORDER_ID\", \"이미 승인 및 취소가 진행된 중복된 주문번호 입니다. 다른 주문번호로 진행해주세요.\"),\n    INVALID_REQUEST(BAD_REQUEST.value(), \"PAYMENTS_CREATE_INVALID_REQUEST\", \"잘못된 요청입니다.\");\n\n    override fun getErrorReason(): ErrorReason = ErrorReason(status, code, reason)\n\n    @Throws(NoSuchFieldException::class)\n    override fun getExplainError(): String {\n        val field = this::class.java.getField(this.name)\n        val annotation = field.getAnnotation(ExplainError::class.java)\n        return if (annotation != null) annotation.value else reason\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/exception/PaymentsEnumNotMatchException.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\nimport band.gosrock.common.exception.GlobalErrorCode\n\nclass PaymentsEnumNotMatchException private constructor() :\n    DuDoongCodeException(GlobalErrorCode.TOSS_PAYMENTS_ENUM_NOT_MATCH) {\n    companion object {\n        @JvmField val EXCEPTION: DuDoongCodeException = PaymentsEnumNotMatchException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/exception/PaymentsUnHandleException.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.exception\n\nimport band.gosrock.common.exception.DuDoongCodeException\nimport band.gosrock.common.exception.GlobalErrorCode\n\nclass PaymentsUnHandleException private constructor() :\n    DuDoongCodeException(GlobalErrorCode.TOSS_PAYMENTS_UNHANDLED) {\n    companion object {\n        @JvmField val EXCEPTION: DuDoongCodeException = PaymentsUnHandleException()\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/exception/TossPaymentsErrorDto.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.exception\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport feign.Response\nimport java.io.IOException\n\nclass TossPaymentsErrorDto {\n    var code: String? = null\n    var message: String? = null\n\n    companion object {\n        @JvmStatic\n        fun from(response: Response): TossPaymentsErrorDto {\n            return try {\n                response.body().asInputStream().use { bodyIs ->\n                    ObjectMapper().readValue(bodyIs, TossPaymentsErrorDto::class.java)\n                }\n            } catch (e: IOException) {\n                throw PaymentsUnHandleException.EXCEPTION\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/kotlin/band/gosrock/infrastructure/outer/api/tossPayments/exception/TransactionGetErrorCode.kt",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.exception\n\nimport band.gosrock.common.annotation.ExplainError\nimport band.gosrock.common.dto.ErrorReason\nimport band.gosrock.common.exception.BaseErrorCode\nimport org.springframework.http.HttpStatus.BAD_REQUEST\n\nenum class TransactionGetErrorCode(\n    private val status: Int,\n    private val code: String,\n    private val reason: String,\n) : BaseErrorCode {\n    UNAUTHORIZED_KEY(BAD_REQUEST.value(), \"PAYMENTS_GET_UNAUTHORIZED_KEY\", \"인증되지 않은 시크릿 키 혹은 클라이언트 키 입니다.\"),\n    FORBIDDEN_CONSECUTIVE_REQUEST(BAD_REQUEST.value(), \"PAYMENTS_GET_FORBIDDEN_CONSECUTIVE_REQUEST\", \"반복적인 요청은 허용되지 않습니다. 잠시 후 다시 시도해주세요.\"),\n    NOT_FOUND_PAYMENT(BAD_REQUEST.value(), \"PAYMENTS_GET_NOT_FOUND_PAYMENT\", \"존재하지 않는 결제 정보 입니다.\"),\n    NOT_FOUND(BAD_REQUEST.value(), \"PAYMENTS_GET_NOT_FOUND\", \"존재하지 않는 정보 입니다.\"),\n    FAILED_PAYMENT_INTERNAL_SYSTEM_PROCESSING(BAD_REQUEST.value(), \"PAYMENTS_GET_FAILED_PAYMENT_INTERNAL_SYSTEM_PROCESSING\", \"결제가 완료되지 않았어요. 다시 시도해주세요.\");\n\n    override fun getErrorReason(): ErrorReason = ErrorReason(status, code, reason)\n\n    @Throws(NoSuchFieldException::class)\n    override fun getExplainError(): String {\n        val field = this::class.java.getField(this.name)\n        val annotation = field.getAnnotation(ExplainError::class.java)\n        return if (annotation != null) annotation.value else reason\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/application-infrastructure.yml",
    "content": "# 공통적용\naws:\n  access-key: ${AWS_ACCESS_KEY:testKey}\n  secret-key: ${AWS_SECRET_KEY:secretKey}\n  s3:\n    bucket: ${AWS_S3_BUCKET:bucket}\n    private-bucket : ${AWS_S3_BUCKET_PRIVATE:bucket-private}\n    base-url: ${AWS_S3_BASE_URL:base-url}\n\nspring:\n  redis:\n    host: ${REDIS_HOST:localhost}\n    port: ${REDIS_PORT:6379}\n    password: ${REDIS_PASSWORD:}\nslack:\n  webhook:\n    token: ${SLACK_WEBHOOK_TOKEN:}\n    id: ${SLACK_WEBHOOK_ID:}\n    service-alarm-channel :  ${SLACK_SERVICE_CHANNEL_ID:}\n    username: ${SLACK_WEBHOOK_USERNAME:DuDoongBot}\n    icon-url: ${SLACK_WEBHOOK_ICON_URL:}\n\nfeign:\n  toss:\n    url : https://api.tosspayments.com\nncp:\n  service-id: ${NCP_SERVICE_ID:}\n  access-key: ${NCP_ACCESS_KEY:}\n  secret-key: ${NCP_SECRET_KEY:}\n  plus-friend-id: ${NCP_PLUS_FRIEND_ID:}\n\n---\nspring:\n  config:\n    activate:\n      on-profile: dev\nlogging:\n  level:\n    band.gosrock.infrastructure.outer.api.* : debug\n---\nspring:\n  config:\n    activate:\n      on-profile: staging\nlogging:\n  level:\n    band.gosrock.infrastructure.outer.api.* : debug\n---\nspring:\n  config:\n    activate:\n      on-profile: prod\nlogging:\n  level:\n    band.gosrock.infrastructure.outer.api.tossPayments.* : debug"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/entranceIssuedTicket.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html\n    xmlns:th=\"http://www.thymeleaf.org\"\n    xmlns:layout=\"http://www.ultraq.net.nz/thymeleaf/layout\"\n    xmlns=\"http://www.w3.org/1999/xhtml\"\n    layout:decorate=\"~{layouts/mailFormat}\"\n>\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  <title></title>\n  <style>\n    * {\n      line-height: 150%;\n      font-family: Pretendard ,AppleSDGothic, apple sd gothic neo, noto sans korean,\n      noto sans korean regular, noto sans cjk kr, noto sans cjk,\n      nanum gothic, malgun gothic, dotum, arial, helvetica, MS Gothic,\n      sans-serif !important;\n      mso-line-height-rule: exactly;\n      line-height: 1.8;\n      -ms-text-size-adjust: 100%;\n      -webkit-text-size-adjust: 100%;\n      word-break: break-word;\n    }\n  </style>\n</head>\n<body style=\"margin: 0; padding: 0\">\n<div layout:fragment=\"content\">\n  <div th:replace=\"fragments/title :: title(title='티켓 입장 확인 안내')\">divider</div>\n\n    <div >\n      <span >안녕하세요</span>\n      <span  th:text=\"${userInfo.name}\" >이름</span>\n      <span >님</span>\n      <br />\n      <span th:text=\"${userInfo.name}\" >이름</span>\n      <span>님이 발급하신 </span>\n    </div>\n  <div th:replace=\"fragments/subTilte :: hostAndEventAndIssuedTicket(eventInfo=${eventInfo}, issuedTicketInfo=${issuedTicketInfo}, text=' 티켓이 입장 처리 되었습니다.')\">\n    이벤트 및 발급 티켓 정보\n  </div>\n  <div th:replace=\"fragments/issuedTicketInfo :: issuedTicketInfo(issuedTicketInfo=${issuedTicketInfo})\">\n    발급 티켓 정보\n  </div>\n  <span>즐거운 관람 되세요!</span>\n\n  <div align=\"right\">\n    <img width=\"163\" style=\"\" src=\"https://asset.dudoong.com/common/duduongs.png\"/>\n  </div>\n\n  <div th:replace=\"fragments/button :: button(href='https://dudoong.com' , text='두둥 바로가기')\">\n    button\n  </div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/eventSettlement.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html\n    xmlns:th=\"http://www.thymeleaf.org\"\n    xmlns:layout=\"http://www.ultraq.net.nz/thymeleaf/layout\"\n    xmlns=\"http://www.w3.org/1999/xhtml\"\n    layout:decorate=\"~{layouts/mailFormat}\"\n>\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  <title></title>\n  <style>\n    * {\n      line-height: 150%;\n      font-family: Pretendard ,AppleSDGothic, apple sd gothic neo, noto sans korean,\n      noto sans korean regular, noto sans cjk kr, noto sans cjk,\n      nanum gothic, malgun gothic, dotum, arial, helvetica, MS Gothic,\n      sans-serif !important;\n      mso-line-height-rule: exactly;\n      line-height: 1.8;\n      -ms-text-size-adjust: 100%;\n      -webkit-text-size-adjust: 100%;\n      word-break: break-word;\n    }\n  </style>\n</head>\n<body style=\"margin: 0; padding: 0\">\n<div layout:fragment=\"content\">\n  <div th:replace=\"fragments/title :: title(title='제휴 호스트 공연 정산 안내')\">divider</div>\n\n  <div style=\"margin-top: 10px ; margin-bottom: 10px ;\">\n    <div>\n      안녕하세요 두둥 정산 관리팀입니다.\n    </div>\n    <div>\n      해당 공연이 종료되어 정산 관련 이메일을 보내드립니다.\n    </div>\n    <br/>\n    <div>\n      첨부 파일로 보내드린 정산서 내역을 확인해 주시고,\n    </div>\n    <br/>\n    <div>\n      <b>입금</b>을 위해\n    </div>\n    <div>\n      입금받으실 분의 성함을 평문으로,\n    </div>\n    <div>\n      통장사본은 첨부하셔서 <b>support@dudoong.com</b> 으로 이메일을 보내주시면 감사하겠습니다.\n    </div>\n    <div>\n      입금받으실 분의 성함과 통장사본의 성함이 일치해야합니다.\n    </div>\n    <br/>\n    <div>\n      문의사항이 있으시면 <a href=\"http://pf.kakao.com/_xiaLWxj\">두둥 카카오톡 채널</a> 또는 010-9476-8640 으로 연락주시면\n    </div>\n    <div>\n      빠르게 답변 드리도록 하겠습니다.\n    </div>\n    <br/>\n    <div>\n      두둥 서비스를 이용해 주셔서 감사합니다.\n    </div>\n  </div>\n\n  <div align=\"right\">\n    <img width=\"163\" style=\"\" src=\"https://asset.dudoong.com/common/duduongs.png\"/>\n  </div>\n\n  <div th:replace=\"fragments/button :: button(href='https://dudoong.com' , text='두둥 바로가기')\">\n    button\n  </div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/fragments/button.html",
    "content": "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:th=\"http://www.thymeleaf.org\">\n<head>\n  <meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\" />\n  <meta content=\"width=device-width, initial-scale=1.0\" name=\"viewport\" />\n  <title></title>\n  <style>\n    * {\n      line-height: 150%;\n    }\n  </style>\n</head>\n<div th:fragment=\"button(href , text)\">\n  <div style=\"padding: 10px 10px\">\n    <table\n        align=\"center\"\n        border=\"0\"\n        cellpadding=\"0\"\n        cellspacing=\"0\"\n        style=\"\n          border-collapse: separate !important;\n          background: #25f3cf;\n          border-radius: 3px;\n          border: 0;\n          mso-table-lspace: 0pt;\n          mso-table-rspace: 0pt;\n          -ms-text-size-adjust: 100%;\n          -webkit-text-size-adjust: 100%;\n          margin: 0 auto;\n          table-layout: fixed;\n        \"\n    >\n      <tbody>\n      <tr>\n        <td\n            align=\"center\"\n            style=\"line-height: 1.5; padding: 14px 100px 11px\"\n        >\n          <a\n              href=\"https://dudoong.com\"\n              th:href=\"${href}\"\n              style=\"\n                  font-size: 14px;\n                  display: inline;\n                  color: rgb(0, 0, 0);\n                  /*background: rgb(80, 148, 250);*/\n                  border-radius: 3px;\n                  text-decoration: none;\n                  outline: 0px;\n                  font-family: AppleSDGothic, apple sd gothic neo,\n                    noto sans korean, noto sans korean regular, noto sans cjk kr,\n                    noto sans cjk, nanum gothic, malgun gothic, dotum, arial,\n                    helvetica, MS Gothic, sans-serif;\n                  text-align: center;\n                  font-weight: bold;\n                \"\n              target=\"_blank\"\n              rel=\"noreferrer noopener\"\n              th:text=\"${text}\"\n          >두둥 바로가기</a\n          >\n        </td>\n      </tr>\n      </tbody>\n    </table>\n  </div>\n</div>\n</html>\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/fragments/divider.html",
    "content": "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:th=\"http://www.thymeleaf.org\">\n<head>\n  <meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\" />\n  <meta content=\"width=device-width, initial-scale=1.0\" name=\"viewport\" />\n  <title></title>\n  <style>\n    * {\n      line-height: 150%;\n    }\n  </style>\n</head>\n<div th:fragment=\"divider\">\n  <table\n      border=\"0\"\n      cellpadding=\"0\"\n      cellspacing=\"0\"\n      style=\"padding: 0px 5px\"\n      width=\"100%\"\n  >\n    <tbody>\n    <tr>\n      <td>\n        <div\n            style=\"\n                height: 0;\n                background: none;\n                padding: 0px;\n                margin-top: 15px;\n                margin-bottom: 25px;\n                border-top-width: 1px;\n                border-top-style: solid;\n                border-top-color: #f0f0f0;\n              \"\n        ></div>\n      </td>\n    </tr>\n    </tbody>\n  </table>\n</div>\n</html>\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/fragments/footer.html",
    "content": "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:th=\"http://www.thymeleaf.org\">\n<head>\n  <meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\" />\n  <meta content=\"width=device-width, initial-scale=1.0\" name=\"viewport\" />\n  <title></title>\n  <style>\n    * {\n      line-height: 150%;\n    }\n  </style>\n</head>\n<div\n    th:fragment=\"footer\"\n    style=\"\n      overflow: hidden;\n      margin-top: 20px;\n      padding-top: 20px;\n      padding-right: 15px;\n      padding-left: 15px;\n      background: #E3E4E8;\n      border: 0;\n      font-family: AppleSDGothic, apple sd gothic neo, noto sans korean,\n        noto sans korean regular, noto sans cjk kr, noto sans cjk, nanum gothic,\n        malgun gothic, dotum, arial, helvetica, MS Gothic, sans-serif !important;\n    \"\n>\n  <table\n      border=\"0\"\n      cellpadding=\"0\"\n      cellspacing=\"0\"\n      style=\"\n          border-collapse: collapse;\n          mso-table-lspace: 0pt;\n          mso-table-rspace: 0pt;\n          -ms-text-size-adjust: 100%;\n          -webkit-text-size-adjust: 100%;\n          table-layout: fixed;\n\n        \"\n      width=\"100%\"\n  >\n    <tbody>\n    <tr>\n      <td\n          style=\"\n                text-align: center;\n                overflow: hidden;\n                clear: both;\n                vertical-align: bottom;\n              \"\n      >\n        <div style=\"text-align: left\">\n          <div style=\"display: inline; text-align: center; \">\n            <img\n                width=\"144\"\n                height=\"40\"\n                src=\"https://asset.dudoong.com/common/en-logo-gray.png\"\n            />\n          </div>\n        </div>\n\n      </td>\n      <td\n          style=\"\n                text-align: right;\n                overflow: hidden;\n                clear: both;\n                vertical-align: bottom;\n              \"\n      >\n        <div style=\"margin: 0; padding: 0; align-self: baseline; text-align: right; \">\n          <a\n              href=\"https://github.com/Gosrock\"\n              rel=\"noreferrer noopener\"\n              style=\"\n          padding-right: 22px;\n          border-image: initial;\n          text-decoration: none;\n        \"\n              target=\"_blank\"\n              title=\"깃허브\"\n          >\n            <img\n                width=\"24px\"\n                loading=\"lazy\"\n                src=\"https://asset.dudoong.com/common/github-gray.png\"\n            /></a>\n          <a\n              href=\"https://instagram.com/dudoong_official\"\n              rel=\"noreferrer noopener\"\n              style=\"\n          padding-right: 22px;\n          text-decoration: none;\n        \"\n              target=\"_blank\"\n              title=\"인스타그램\"\n          ><img\n              width=\"24px\"\n              loading=\"lazy\"\n              src=\"https://asset.dudoong.com/common/instagram-gray.png\"\n          /> </a\n          ><a\n            href=\"http://pf.kakao.com/_xiaLWxj\"\n            rel=\"noreferrer noopener\"\n            style=\"\n          padding: 0px 0px;\n          border-image: initial;\n          text-decoration: none;\n        \"\n            target=\"_blank\"\n            title=\"카카오톡 채널\"\n        ><img\n            width=\"24px\"\n            loading=\"lazy\"\n            src=\"https://asset.dudoong.com/common/kakao-gray.png\"\n        />\n        </a>\n        </div>\n      </td>\n    </tr>\n    </tbody>\n  </table>\n\n\n\n\n\n\n\n\n\n  <div\n      style=\"text-align: left; padding-top: 15px; padding-bottom: 15px; font-size: 10px ; color: #7A7A80\"\n  >\n    <div>두둥스튜디오 | 서울시 강서구 강서로 266 129동 503호</div>\n    <div>\n      대표 : 이찬진 | TEL : 0507-0177-0711 | Email : support@dudoong.com\n    </div>\n    <div>\n      사업자 번호 : 469-21-01595 | 통신판매업 신고번호 : 2022-서울강서-3669\n    </div>\n    <div\n        style=\"\n                height: 0;\n                background: none;\n                padding: 0px;\n                margin-top: 20px;\n                margin-bottom: 10px;\n                border-top-width: 1px;\n                border-top-style: solid;\n                border-top-color: #7A7A80;\n              \"\n    ></div>\n      <a>© Dudoong. 2023 All rights reserved.</a>\n    <br/>\n      <div style=\"display: flex\">\n        <a\n            href=\"https://dudoong.com/meta/privacy\"\n            rel=\"noreferrer noopener\"\n            style=\"\n            text-decoration: none;\n            color: #7A7A80;\n            display: inline;\n            padding-right: 10px;\n          \"\n            target=\"_blank\"\n        >개인정보 처리방침</a\n        >\n        <a style=\" padding-right: 10px;\">|</a>\n        <a\n            href=\"https://dudoong.com/meta/term\"\n            rel=\"noreferrer noopener\"\n            style=\"\n            text-decoration: none;\n            color: #7A7A80;\n            display: inline;\n             padding-right: 10px;\n          \"\n            target=\"_blank\"\n        >서비스 이용약관</a\n        >\n        <a style=\" padding-right: 10px;\">|</a>\n        <a\n            href=\"https://dudoong.com/mypage\"\n            rel=\"noreferrer noopener\"\n            style=\"\n            text-decoration: none;\n            color: #7A7A80;\n            display: inline;\n          \"\n            target=\"_blank\"\n        >수신거부</a\n        >\n      </div>\n\n  </div>\n</div>\n</html>\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/fragments/header.html",
    "content": "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:th=\"http://www.thymeleaf.org\">\n<head>\n  <meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\" />\n  <meta content=\"width=device-width, initial-scale=1.0\" name=\"viewport\" />\n  <title></title>\n  <style>\n    * {\n      line-height: 150%;\n    }\n  </style>\n</head>\n<div th:fragment=\"header\">\n  <div style=\"text-align: center; padding: 25px\">\n    <img\n        width=\"244\"\n        src=\"https://asset.dudoong.com/common/en-banner-black.png\"\n    />\n  </div>\n  <div style=\"background: #c7c7cb; height: 1px; margin-bottom: 20px\"></div>\n</div>\n</html>\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/fragments/issuedTicketInfo.html",
    "content": "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:th=\"http://www.thymeleaf.org\">\n<head>\n  <meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\" />\n  <meta content=\"width=device-width, initial-scale=1.0\" name=\"viewport\" />\n  <title></title>\n  <style>\n    * {\n      line-height: 150%;\n    }\n  </style>\n</head>\n<div th:fragment=\"issuedTicketInfo(issuedTicketInfo)\"\n     style=\"\n      overflow: hidden;\n      padding: 0px;\n      margin-top: 20px;\n      border: 0;\n      font-weight: bold;\n      font-size: 14px;\n      font-family: Pretendard , AppleSDGothic, apple sd gothic neo, noto sans korean,\n        noto sans korean regular, noto sans cjk kr, noto sans cjk, nanum gothic,\n        malgun gothic, dotum, arial, helvetica, MS Gothic, sans-serif !important;\n    \"\n>\n  <div style=\"font-weight: bold; background: #F8F8FA; padding: 10px ; margin-top: 10px;margin-bottom: 10px\">\n    <div >\n      <span  >티켓 번호 : </span>\n      <span  th:text=\"${issuedTicketInfo.issuedTicketNo}\" >티켓 번호</span>\n    </div>\n    <div >\n      <span  >티켓명 : </span>\n      <span  th:text=\"${issuedTicketInfo.ticketName}\" >티켓명</span>\n    </div>\n    <div >\n      <span  >발급 일시 : </span>\n      <span  th:text=\"${#temporals.format(issuedTicketInfo.createdAt, 'yyyy-MM-dd HH:mm')}\" >발급 일시</span>\n    </div>\n    <div >\n      <span  >티켓 상태 : </span>\n      <span  th:text=\"${issuedTicketInfo.issuedTicketStatus}\" >티켓 상태</span>\n    </div>\n    <div>\n      <span>가격 : </span>\n      <span th:text=\"${issuedTicketInfo.money}\">가격</span>\n    </div>\n  </div>\n</div>\n</html>\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/fragments/orderInfo.html",
    "content": "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:th=\"http://www.thymeleaf.org\">\n<head>\n  <meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\" />\n  <meta content=\"width=device-width, initial-scale=1.0\" name=\"viewport\" />\n  <title></title>\n  <style>\n    * {\n      line-height: 150%;\n    }\n  </style>\n</head>\n<div th:fragment=\"orderInfo(orderInfo)\"\n     style=\"\n      overflow: hidden;\n      padding: 0px;\n      margin-top: 20px;\n      border: 0;\n      font-weight: bold;\n      font-size: 14px;\n      font-family: Pretendard , AppleSDGothic, apple sd gothic neo, noto sans korean,\n        noto sans korean regular, noto sans cjk kr, noto sans cjk, nanum gothic,\n        malgun gothic, dotum, arial, helvetica, MS Gothic, sans-serif !important;\n    \"\n>\n  <div style=\"font-weight: bold; background: #F8F8FA; padding: 10px ; margin-top: 10px;margin-bottom: 10px\">\n    <div >\n      <span  >주문 이름 : </span>\n      <span  th:text=\"${orderInfo.name}\" >호스트이름</span>\n    </div>\n    <div >\n      <span  >수량 : </span>\n      <span  th:text=\"${orderInfo.quantity}\" >수량</span>\n      <span  >매 </span>\n    </div>\n    <div >\n      <span  >가격 : </span>\n      <span  th:text=\"${orderInfo.money}\" >가격</span>\n    </div>\n    <div >\n      <span  >주문 일시 : </span>\n      <span  th:text=\"${#temporals.format(orderInfo.createAt, 'yyyy-MM-dd HH:mm')}\" >주문일시</span>\n    </div>\n  </div>\n</div>\n</html>\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/fragments/subTilte.html",
    "content": "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:th=\"http://www.thymeleaf.org\">\n<head>\n  <meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\" />\n  <meta content=\"width=device-width, initial-scale=1.0\" name=\"viewport\" />\n  <title></title>\n  <style>\n    * {\n      line-height: 150%;\n    }\n  </style>\n</head>\n<div th:fragment=\"hostAndEvent(eventInfo,text)\"\n     style=\"\n      overflow: hidden;\n      padding: 0px;\n      border: 0;\n      font-size: 14px;\n      font-family: Pretendard , AppleSDGothic, apple sd gothic neo, noto sans korean,\n        noto sans korean regular, noto sans cjk kr, noto sans cjk, nanum gothic,\n        malgun gothic, dotum, arial, helvetica, MS Gothic, sans-serif !important;\n    \"\n>\n  <div >\n    <span  th:text=\"${eventInfo.hostName}\" >호스트이름</span>\n    <span  > </span>\n    <span  th:text=\"${eventInfo.eventName}\" >이벤트 이름</span>\n    <span th:text=\"${text}\"> 텍스트 </span>\n    <br />\n  </div>\n</div>\n\n<div th:fragment=\"host(hostName,text)\"\n     style=\"\n      overflow: hidden;\n      padding: 0px;\n      border: 0;\n      font-size: 14px;\n      font-family: Pretendard , AppleSDGothic, apple sd gothic neo, noto sans korean,\n        noto sans korean regular, noto sans cjk kr, noto sans cjk, nanum gothic,\n        malgun gothic, dotum, arial, helvetica, MS Gothic, sans-serif !important;\n    \"\n>\n  <div >\n    <span  th:text=\"${hostName}\" >호스트이름</span>\n    <span > 호스트 </span>\n    <span th:text=\"${text}\"> 텍스트 </span>\n    <br />\n  </div>\n</div>\n\n<div th:fragment=\"hostAndEventAndIssuedTicket(eventInfo, issuedTicketInfo, text)\"\n     style=\"...\"\n>\n  <div>\n    <span th:text=\"${eventInfo.hostName}\" >호스트이름</span>\n    <span th:text=\"${eventInfo.eventName}\">이벤트 이름</span>\n    <span>의</span>\n    <span th:text=\"${issuedTicketInfo.issuedTicketNo}\">발급 티켓 번호</span>\n    <span th:text=\"${text}\"> 텍스트 </span>\n    <br />\n  </div>\n</div>\n\n</html>\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/fragments/title.html",
    "content": "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:th=\"http://www.thymeleaf.org\">\n<head>\n  <meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\" />\n  <meta content=\"width=device-width, initial-scale=1.0\" name=\"viewport\" />\n  <title></title>\n  <style>\n    * {\n      line-height: 150%;\n    }\n  </style>\n</head>\n<div th:fragment=\"title(title)\"\n     style=\"\n      overflow: hidden;\n      padding: 0px;\n      margin-top: 20px;\n      border: 0;\n      font-weight: bold;\n      font-size: 24px;\n      font-family: 'Gmarket Sans' , AppleSDGothic, apple sd gothic neo, noto sans korean,\n        noto sans korean regular, noto sans cjk kr, noto sans cjk, nanum gothic,\n        malgun gothic, dotum, arial, helvetica, MS Gothic, sans-serif !important;\n    \"\n>\n  <div style=\"padding: 0px 5px\">\n    <div>두둥</div>\n    <div th:text=\"${title}\"\n         style=\"color: #6B36DC\">타이틀</div>\n  </div>\n  <div th:replace=\"fragments/divider :: divider\">divider</div>\n</div>\n</html>\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/hostInvite.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html\n    xmlns:th=\"http://www.thymeleaf.org\"\n    xmlns:layout=\"http://www.ultraq.net.nz/thymeleaf/layout\"\n    xmlns=\"http://www.w3.org/1999/xhtml\"\n    layout:decorate=\"~{layouts/mailFormat}\"\n>\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  <title></title>\n  <style>\n    * {\n      line-height: 150%;\n      font-family: Pretendard ,AppleSDGothic, apple sd gothic neo, noto sans korean,\n      noto sans korean regular, noto sans cjk kr, noto sans cjk,\n      nanum gothic, malgun gothic, dotum, arial, helvetica, MS Gothic,\n      sans-serif !important;\n      mso-line-height-rule: exactly;\n      line-height: 1.8;\n      -ms-text-size-adjust: 100%;\n      -webkit-text-size-adjust: 100%;\n      word-break: break-word;\n    }\n  </style>\n</head>\n<body style=\"margin: 0; padding: 0\">\n<div layout:fragment=\"content\">\n  <div th:replace=\"fragments/title :: title(title='호스트 초대 알림')\">divider</div>\n\n    <div >\n      <span >안녕하세요</span>\n      <span  th:text=\"${userInfo.name}\" >이름</span>\n      <span >님</span>\n      <br />\n    </div>\n  <div th:replace=\"fragments/subTilte :: host(hostName=${hostName},text='관리자 초대가 되어 안내 메일 드립니다.')\">\n    이벤트 정보\n  </div>\n  <div style=\"font-weight: bold; background: #F8F8FA; padding: 10px ; margin-top: 10px;margin-bottom: 10px\">\n    <div >\n      <span  >대상 계정 : </span>\n      <span  th:text=\"${userInfo.email}\" >대상계정 이메일</span>\n    </div>\n    <div >\n      <span  >권한 : </span>\n      <span  th:text=\"${role}\" >권한</span>\n    </div>\n  </div>\n\n\n  <div style=\"margin-top: 10px ; margin-bottom: 10px ;\">\n    <div>\n      권한은 마스터, 매니저, 게스트로 나누어집니다.\n    </div>\n    <div>\n      게스트는 관리자 공연에서 조회의 권한을 가집니다.\n    </div>\n    <div>\n      매니저는 게스트의 권한과 관리자 공연에서 수정과 생성 권한을 가집니다.\n    </div>\n    <div>\n      마스터는 매니저의 권한과 호스트의 멤버관리 권한을 가지고 있습니다.\n    </div>\n  </div>\n\n  <div align=\"right\">\n    <img width=\"163\" style=\"\" src=\"https://asset.dudoong.com/common/duduongs.png\"/>\n  </div>\n\n  <div th:replace=\"fragments/button :: button(href='https://dudoong.com' , text='두둥 바로가기')\">\n    button\n  </div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/layouts/mailFormat.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns:th=\"http://www.thymeleaf.org\" xmlns:layout=\"http://www.ultraq.net.nz/thymeleaf/layout\"  xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  <title></title>\n  <style>\n    * {\n      line-height: 150%;\n    }\n  </style>\n</head>\n<body style=\"margin: 0; padding: 0\">\n<div class=\"mail_view_contents\">\n  <div class=\"mail_view_contents_inner\">\n    <div>\n      <div\n          style=\"\n              width: 100%;\n              padding: 40px 0;\n              background-color: #ffffff;\n              margin: 0 auto;\n            \"\n      >\n        <table\n            style=\"\n        overflow: hidden;\n        margin: 0px auto;\n        width: 100%;\n        max-width: 630px;\n        clear: both;\n        background: none;\n        border: 0;\n      \"\n        >\n          <tbody>\n          <tr>\n            <td\n                style=\"\n              line-height: 1.8;\n              border-width: 0px;\n              font-size: 14px;\n            \"\n            >\n        <div th:replace = \"fragments/header :: header\">\n          header\n        </div>\n        <div layout:fragment=\"content\"></div>\n\n        <div th:replace = \"fragments/footer :: footer\">\n          footer\n        </div>\n            </td>\n          </tr>\n          </tbody>\n        </table>\n    </div>\n  </div>\n</div>\n</div>\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/orderApproveConfirm.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html\n    xmlns:th=\"http://www.thymeleaf.org\"\n    xmlns:layout=\"http://www.ultraq.net.nz/thymeleaf/layout\"\n    xmlns=\"http://www.w3.org/1999/xhtml\"\n    layout:decorate=\"~{layouts/mailFormat}\"\n>\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  <title></title>\n  <style>\n    * {\n      line-height: 150%;\n      font-family: Pretendard ,AppleSDGothic, apple sd gothic neo, noto sans korean,\n      noto sans korean regular, noto sans cjk kr, noto sans cjk,\n      nanum gothic, malgun gothic, dotum, arial, helvetica, MS Gothic,\n      sans-serif !important;\n      mso-line-height-rule: exactly;\n      line-height: 1.8;\n      -ms-text-size-adjust: 100%;\n      -webkit-text-size-adjust: 100%;\n      word-break: break-word;\n    }\n  </style>\n</head>\n<body style=\"margin: 0; padding: 0\">\n<div layout:fragment=\"content\">\n  <div th:replace=\"fragments/title :: title(title='주문 승인 확인 안내')\">divider</div>\n\n    <div >\n      <span >안녕하세요</span>\n      <span  th:text=\"${userInfo.name}\" >이름</span>\n      <span >님</span>\n      <br />\n    </div>\n  <div th:replace=\"fragments/subTilte :: hostAndEvent(eventInfo=${eventInfo},text=' 주문이 승인되었습니다.')\">\n    이벤트 정보\n  </div>\n  <div th:replace=\"fragments/orderInfo :: orderInfo(orderInfo=${orderInfo})\">\n    주문 정보\n  </div>\n\n  <div style=\"margin-top: 10px ; margin-bottom: 10px\">\n  <div>\n    원할한 입장을 위해 QR코드를 미리 준비해 주세요\n  </div>\n  <div>\n    마이페이지 -> 내 예매 내역 -> 예매확인 -> QR 코드 보기\n  </div>\n  <div>\n    에서 각 티켓의 QR코드를 확인 할 수 있습니다.\n  </div>\n</div>\n\n  <div style=\"margin-top: 10px ; margin-bottom: 10px ;color: #7A7A80\">\n    <div>\n      철회기한 이전까지 수수료 없이 환불 가능합니다.\n    </div>\n    <div>\n      공연 입장확인이 된 티켓이 있을경우 환불이 불가합니다.\n    </div>\n    <div>\n      환불 방법 : 티켓 예매 상세내역 > 예매취소\n    </div>\n    <div>\n      환불 금액 : 당사에서는 주문에 대해 즉시 취소를 하고,\n    </div>\n    <div>\n      취소 금액에 대한 입금은 카드사 영업일 기준 4~5일이 소요될 수 있습니다.\n    </div>\n  </div>\n\n  <div align=\"right\">\n    <img width=\"163\" style=\"\" src=\"https://asset.dudoong.com/common/duduongs.png\"/>\n  </div>\n\n  <div th:replace=\"fragments/button :: button(href='https://dudoong.com' , text='두둥 바로가기')\">\n    button\n  </div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/orderApproveRequest.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html\n    xmlns:th=\"http://www.thymeleaf.org\"\n    xmlns:layout=\"http://www.ultraq.net.nz/thymeleaf/layout\"\n    xmlns=\"http://www.w3.org/1999/xhtml\"\n    layout:decorate=\"~{layouts/mailFormat}\"\n>\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  <title></title>\n  <style>\n    * {\n      line-height: 150%;\n      font-family: Pretendard ,AppleSDGothic, apple sd gothic neo, noto sans korean,\n      noto sans korean regular, noto sans cjk kr, noto sans cjk,\n      nanum gothic, malgun gothic, dotum, arial, helvetica, MS Gothic,\n      sans-serif !important;\n      mso-line-height-rule: exactly;\n      line-height: 1.8;\n      -ms-text-size-adjust: 100%;\n      -webkit-text-size-adjust: 100%;\n      word-break: break-word;\n    }\n  </style>\n</head>\n<body style=\"margin: 0; padding: 0\">\n<div layout:fragment=\"content\">\n  <div th:replace=\"fragments/title :: title(title='주문 승인 요청 안내')\">divider</div>\n\n    <div >\n      <span >안녕하세요</span>\n      <span  th:text=\"${userInfo.name}\" >이름</span>\n      <span >님</span>\n      <br />\n    </div>\n  <div th:replace=\"fragments/subTilte :: hostAndEvent(eventInfo=${eventInfo},text=' 주문 승인 요청이 완료 되었습니다.')\">\n    이벤트 정보\n  </div>\n  <div th:replace=\"fragments/orderInfo :: orderInfo(orderInfo=${orderInfo})\">\n    주문 정보\n  </div>\n\n  <div style=\"margin-top: 10px ; margin-bottom: 10px\">\n    <div>\n      호스트 관리자가 주문을 승인 할 때 까지 기다려주세요!\n    </div>\n    <div>\n      승인이 완료되면 알림 메일을 보내 드립니다.\n    </div>\n    <div>\n      장기간 승인이 이루어지지 않는다면, 호스트 연락처를 통해 주문을 확인해 보세요!\n    </div>\n  </div>\n\n  <div align=\"right\">\n    <img width=\"163\" style=\"\" src=\"https://asset.dudoong.com/common/duduongs.png\"/>\n  </div>\n\n  <div th:replace=\"fragments/button :: button(href='https://dudoong.com' , text='두둥 바로가기')\">\n    button\n  </div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/orderPaymentDone.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html\n    xmlns:th=\"http://www.thymeleaf.org\"\n    xmlns:layout=\"http://www.ultraq.net.nz/thymeleaf/layout\"\n    xmlns=\"http://www.w3.org/1999/xhtml\"\n    layout:decorate=\"~{layouts/mailFormat}\"\n>\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  <title></title>\n  <style>\n    * {\n      line-height: 150%;\n      font-family: Pretendard ,AppleSDGothic, apple sd gothic neo, noto sans korean,\n      noto sans korean regular, noto sans cjk kr, noto sans cjk,\n      nanum gothic, malgun gothic, dotum, arial, helvetica, MS Gothic,\n      sans-serif !important;\n      mso-line-height-rule: exactly;\n      line-height: 1.8;\n      -ms-text-size-adjust: 100%;\n      -webkit-text-size-adjust: 100%;\n      word-break: break-word;\n    }\n  </style>\n</head>\n<body style=\"margin: 0; padding: 0\">\n<div layout:fragment=\"content\">\n  <div th:replace=\"fragments/title :: title(title='주문 완료 안내')\">divider</div>\n\n    <div >\n      <span >안녕하세요</span>\n      <span  th:text=\"${userInfo.name}\" >이름</span>\n      <span >님</span>\n      <br />\n    </div>\n  <div th:replace=\"fragments/subTilte :: hostAndEvent(eventInfo=${eventInfo},text=' 주문 완료 되었습니다.')\">\n    이벤트 정보\n  </div>\n  <div th:replace=\"fragments/orderInfo :: orderInfo(orderInfo=${orderInfo})\">\n    주문 정보\n  </div>\n\n  <div style=\"margin-top: 10px ; margin-bottom: 10px\">\n  <div>\n    원할한 입장을 위해 QR코드를 미리 준비해 주세요\n  </div>\n  <div>\n    마이페이지 -> 내 예매 내역 -> 예매확인 -> QR 코드 보기\n  </div>\n  <div>\n    에서 각 티켓의 QR코드를 확인 할 수 있습니다.\n  </div>\n</div>\n\n  <div style=\"margin-top: 10px ; margin-bottom: 10px ;color: #7A7A80\">\n    <div>\n      철회기한 이전까지 수수료 없이 환불 가능합니다.\n    </div>\n    <div>\n      공연 입장확인이 된 티켓이 있을경우 환불이 불가합니다.\n    </div>\n    <div>\n      환불 방법 : 티켓 예매 상세내역 > 예매취소\n    </div>\n    <div>\n      환불 금액 : 당사에서는 주문에 대해 즉시 취소를 하고,\n    </div>\n    <div>\n      취소 금액에 대한 입금은 카드사 영업일 기준 4~5일이 소요될 수 있습니다.\n    </div>\n  </div>\n\n  <div align=\"right\">\n    <img width=\"163\" style=\"\" src=\"https://asset.dudoong.com/common/duduongs.png\"/>\n  </div>\n\n  <div th:replace=\"fragments/button :: button(href='https://dudoong.com' , text='두둥 바로가기')\">\n    button\n  </div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/orderWithdrawCancel.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html\n    xmlns:th=\"http://www.thymeleaf.org\"\n    xmlns:layout=\"http://www.ultraq.net.nz/thymeleaf/layout\"\n    xmlns=\"http://www.w3.org/1999/xhtml\"\n    layout:decorate=\"~{layouts/mailFormat}\"\n>\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  <title></title>\n  <style>\n    * {\n      line-height: 150%;\n      font-family: Pretendard ,AppleSDGothic, apple sd gothic neo, noto sans korean,\n      noto sans korean regular, noto sans cjk kr, noto sans cjk,\n      nanum gothic, malgun gothic, dotum, arial, helvetica, MS Gothic,\n      sans-serif !important;\n      mso-line-height-rule: exactly;\n      line-height: 1.8;\n      -ms-text-size-adjust: 100%;\n      -webkit-text-size-adjust: 100%;\n      word-break: break-word;\n    }\n  </style>\n</head>\n<body style=\"margin: 0; padding: 0\">\n<div layout:fragment=\"content\">\n  <div th:replace=\"fragments/title :: title(title='주문 철회 안내')\">divider</div>\n\n    <div >\n      <span >안녕하세요</span>\n      <span  th:text=\"${userInfo.name}\" >이름</span>\n      <span >님</span>\n      <br />\n    </div>\n  <div th:replace=\"fragments/subTilte :: hostAndEvent(eventInfo=${eventInfo},text=' 주문이 호스트 관리자에 의해 취소되어 안내드립니다.')\">\n    이벤트 정보\n  </div>\n  <div th:replace=\"fragments/orderInfo :: orderInfo(orderInfo=${orderInfo})\">\n    주문 정보\n  </div>\n\n  <div style=\"margin-top: 10px ; margin-bottom: 10px\">\n  <div>\n    철회 사유에 대한 문의는 호스트 연락처를 통해 부탁드립니다.\n  </div>\n</div>\n\n  <div style=\"margin-top: 10px ; margin-bottom: 10px ;color: #7A7A80\">\n    <div>\n      당사에서는 주문에 대해 즉시 취소를 하고,\n    </div>\n    <div>\n      취소 금액에 대한 입금은 카드사 영업일 기준 4~5일이 소요될 수 있습니다.\n    </div>\n  </div>\n\n  <div align=\"right\">\n    <img width=\"163\" style=\"\" src=\"https://asset.dudoong.com/common/duduongs.png\"/>\n  </div>\n\n  <div th:replace=\"fragments/button :: button(href='https://dudoong.com' , text='두둥 바로가기')\">\n    button\n  </div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/orderWithdrawRefund.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html\n    xmlns:th=\"http://www.thymeleaf.org\"\n    xmlns:layout=\"http://www.ultraq.net.nz/thymeleaf/layout\"\n    xmlns=\"http://www.w3.org/1999/xhtml\"\n    layout:decorate=\"~{layouts/mailFormat}\"\n>\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  <title></title>\n  <style>\n    * {\n      line-height: 150%;\n      font-family: Pretendard ,AppleSDGothic, apple sd gothic neo, noto sans korean,\n      noto sans korean regular, noto sans cjk kr, noto sans cjk,\n      nanum gothic, malgun gothic, dotum, arial, helvetica, MS Gothic,\n      sans-serif !important;\n      mso-line-height-rule: exactly;\n      line-height: 1.8;\n      -ms-text-size-adjust: 100%;\n      -webkit-text-size-adjust: 100%;\n      word-break: break-word;\n    }\n  </style>\n</head>\n<body style=\"margin: 0; padding: 0\">\n<div layout:fragment=\"content\">\n  <div th:replace=\"fragments/title :: title(title='주문 철회 안내')\">divider</div>\n\n    <div >\n      <span >안녕하세요</span>\n      <span  th:text=\"${userInfo.name}\" >이름</span>\n      <span >님</span>\n      <br />\n    </div>\n  <div th:replace=\"fragments/subTilte :: hostAndEvent(eventInfo=${eventInfo},text=' 주문이 환불되어 안내 드립니다.')\">\n    이벤트 정보\n  </div>\n  <div th:replace=\"fragments/orderInfo :: orderInfo(orderInfo=${orderInfo})\">\n    주문 정보\n  </div>\n\n\n  <div style=\"margin-top: 10px ; margin-bottom: 10px ;color: #7A7A80\">\n    <div>\n      당사에서는 주문에 대해 즉시 취소를 하고,\n    </div>\n    <div>\n      취소 금액에 대한 입금은 카드사 영업일 기준 4~5일이 소요될 수 있습니다.\n    </div>\n  </div>\n\n  <div align=\"right\">\n    <img width=\"163\" style=\"\" src=\"https://asset.dudoong.com/common/duduongs.png\"/>\n  </div>\n\n  <div th:replace=\"fragments/button :: button(href='https://dudoong.com' , text='두둥 바로가기')\">\n    button\n  </div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/settlement.html",
    "content": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n  <meta charset=\"utf-8\"/>\n  <meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\"/>\n  <meta charset='UTF-8'/>\n  <meta content='Content-type: text/html; charset=UTF-8' name='http-equiv'/>\n  <title></title>\n  <style type=\"text/css\">\n    /*@font-face {*/\n    /*  src: url('NanumBarunGothic.ttf');   !* 폰트경로지정 *!*/\n    /*  -fs-pdf-font-embed: embed;*/\n    /*  -fs-pdf-font-encoding: Identity-H;*/\n    /*}*/\n    body {\n      width: 100%;\n      font-size: 12px;\n      font-weight: lighter;\n      color:#000;\n      line-height:180%;\n      font-family: NanumBarunGothic,serif;\n    }\n\n    img {border: 0 none;}\n\n    table {\n      width: 100%;\n      border: 0.5px solid #c7c7cb;\n      border-collapse: collapse;\n    }\n    th, td {\n      border: 0.5px solid #c7c7cb;\n      padding: 10px;\n      word-break:break-all;\n    }\n  </style>\n</head>\n\n<body>\n<div>\n  <div style=\"text-align: center; padding: 25px\">\n    <img\n        width=\"244\"\n        alt=\"en-banner-black\"\n        src=\"https://asset.dudoong.com/common/en-banner-black.png\"\n    />\n  </div>\n  <div style=\"background: #c7c7cb; height: 1px; margin-bottom: 20px\"></div>\n</div>\n<!--<div-->\n<!--     style=\"-->\n<!--      /*overflow: hidden;*/-->\n<!--      padding: 0px;-->\n<!--      margin-top: 20px;-->\n<!--      line-height: 35px;-->\n<!--      border: 0;-->\n<!--      font-weight: bold;-->\n<!--      font-size: 24px;-->\n<!--    \"-->\n<!--&gt;-->\n<!--  <div style=\"padding: 0px 5px\">-->\n<!--    <div>두둥</div>-->\n<!--    <div style=\"color: #6B36DC\">정산서</div>-->\n<!--  </div>-->\n<!--</div>-->\n<table\n    style=\"border-collapse: collapse; width: 98.4277%; height: 248px\"\n    border=\"1\"\n>\n  <tbody>\n  <tr style=\"background: #c7c7cb\">\n    <td colspan='2' style=\"width: 50%; font-weight: bold\">공연 정보</td>\n  </tr>\n  <tr style=\"\">\n    <td style=\"width: 50%; \">공연 제목</td>\n    <td style=\"width: 50%;  text-align: right\" th:text=\"${eventTitle}\">제목</td>\n  </tr>\n  <tr style=\"\">\n    <td style=\"width: 50%; \">호스트 마스터 이름</td>\n    <td style=\"width: 50%;  text-align: right\" th:text=\"${hostName}\">호스트 이름</td>\n  </tr>\n  <tr style=\"\">\n    <td style=\"width: 50%; \">정산 지급 예정일</td>\n    <td style=\"width: 50%;  text-align: right\" th:text=\"${#temporals.format(settlementAt, 'yyyy년 MM월 dd일')}\">2023년 03월 31일</td>\n  </tr>\n  <tr style=\"\">\n    <td colspan='2' style=\"width: 50%; \"></td>\n  </tr>\n  <tr style=\"background: #c7c7cb\">\n    <td colspan='2' style=\"width: 50%; font-weight: bold\">판매 금액</td>\n  </tr>\n  <tr style=\"\">\n    <td style=\"width: 50%; \">두둥 티켓</td>\n    <td style=\"width: 50%;  text-align: right\" th:text=\"${dudoongTicketAmount}\"><br /></td>\n  </tr>\n  <tr style=\"font-weight: bold\">\n    <td style=\"width: 50%; \">A. 유료 티켓</td>\n    <td style=\"width: 50%;  text-align: right\" th:text=\"${pgTicketAmount}\"><br /></td>\n  </tr>\n  <tr style=\"\">\n    <td style=\"width: 50%; \">총 판매 금액</td>\n    <td style=\"width: 50%;  text-align: right\" th:text=\"${totalAmount}\"><br /></td>\n  </tr>\n  <tr style=\"\">\n    <td colspan='2' style=\"width: 50%; \"></td>\n  </tr>\n  <tr style=\"background: #c7c7cb\">\n    <td colspan='2' style=\"width: 50%; font-weight: bold\">수수료</td>\n  </tr>\n  <tr>\n    <td style=\"width: 50%\">두둥 수수료</td>\n    <td style=\"width: 50%; text-align: right\" th:text=\"${dudoongFee}\"><br /></td>\n  </tr>\n  <tr>\n    <td style=\"width: 50%\">결제 송금 관련 수수료</td>\n    <td style=\"width: 50%; text-align: right\" th:text=\"${pgFee}\"><br /></td>\n  </tr>\n  <tr style=\"font-weight: bold\">\n    <td style=\"width: 50%\">B. 수수료 총계</td>\n    <td style=\"width: 50%; text-align: right\" th:text=\"${totalFee}\"><br /></td>\n  </tr>\n  <tr style=\"font-weight: bold\">\n    <td style=\"width: 50%\">C. 부가가치세</td>\n    <td style=\"width: 50%; text-align: right\" th:text=\"${totalFeeVat}\"><br /></td>\n  </tr>\n  <tr>\n    <td colspan='2' style=\"width: 50%\"></td>\n  </tr>\n  <tr style=\"font-weight: bold\">\n    <td style=\"width: 50%\">최종 정산 금액 ( A - B - C )</td>\n    <td style=\"width: 50%; text-align: right\" th:text=\"${totalSettlement}\"><br /></td>\n  </tr>\n  </tbody>\n</table>\n<div style=\"padding-top: 20px; font-weight: bold\">\n  <div>\n    수수료 총계 금액에 대해서 세금 계산서를 발행드릴 예정입니다.\n  </div>\n  <div>\n    두둥의 매출인 수수료에 대해서는 매출 발생이 확정된 공연 종료일을 기준으로 매출이 발생한 것으로 봅니다.\n  </div>\n<!--  <div>-->\n<!--    이에 대한 증빙으로 두둥에서 호스트 앞으로 수수료에 대한 전자세금계산서를 발행해 드릴때-->\n<!--    공연의 종료일을 기준으로 발급해드립니다.-->\n<!--  </div>-->\n  <div>\n    정산드린 금액에 대해선 입금 받는 호스트 분께서 성실하게 세금에대해 신고할 의무가 있습니다.\n  </div>\n</div>\n<br/>\n<div style=\"padding-right: 20px\">\n\n  <div style=\"text-align: right;\">\n    두둥 스튜디오\n  </div>\n  <div style=\"text-align: right;\" th:text=\"${#temporals.format(now, 'yyyy년 MM월 dd일')}\">\n    yyyy년 MM월 dd일\n  </div>\n</div>\n\n</body>\n</html>"
  },
  {
    "path": "DuDoong-Infrastructure/src/main/resources/templates/signUp.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html\n    xmlns:th=\"http://www.thymeleaf.org\"\n    xmlns:layout=\"http://www.ultraq.net.nz/thymeleaf/layout\"\n    xmlns=\"http://www.w3.org/1999/xhtml\"\n    layout:decorate=\"~{layouts/mailFormat}\"\n>\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  <title></title>\n  <style>\n    * {\n      line-height: 150%;\n      font-family: AppleSDGothic, apple sd gothic neo, noto sans korean,\n      noto sans korean regular, noto sans cjk kr, noto sans cjk,\n      nanum gothic, malgun gothic, dotum, arial, helvetica, MS Gothic,\n      sans-serif !important;\n      mso-line-height-rule: exactly;\n      line-height: 1.8;\n      -ms-text-size-adjust: 100%;\n      -webkit-text-size-adjust: 100%;\n      word-break: break-word;\n    }\n  </style>\n</head>\n<body style=\"margin: 0; padding: 0\">\n<div layout:fragment=\"content\">\n  <div\n      style=\"padding: 0px 15px; font-size: 14px; color: #2b2d36\"\n      width=\"100%\"\n  >\n    <div style=\"text-align: center\">\n          <span\n              style=\"color: rgb(43, 45, 54); font-size: 20px; font-weight: bold\"\n              th:text=\"${username}\"\n          >이름</span\n          >\n      <span style=\"color: rgb(43, 45, 54); font-size: 20px\"\n      ><span style=\"font-weight: bold\">님,</span>\n            &nbsp;\n          </span>\n      <span style=\"color: rgb(43, 45, 54); font-size: 20px\"\n      >두둥에 가입하신 것을 환영합니다!</span\n      >\n      <br />\n    </div>\n  </div>\n\n  <div\n      style=\"padding: 0px 15px; font-size: 14px; color: #2b2d36\"\n      width=\"100%\"\n  >\n    <div style=\"text-align: center; color: rgb(82, 84, 99)\">\n      <div>두둥은 누구나 자유롭게</div>\n      <div>공연을 홍보하고 인원을 모집할 수 있는 서비스 입니다.</div>\n      <div>\n        호스트가 되어 밴드, 뮤지컬 , 버스킹등 공연을 열고 홍보하세요!\n      </div>\n    </div>\n  </div>\n  <div th:replace=\"fragments/divider :: divider\">divider</div>\n  <div style=\"text-align: center\">\n    <table\n        border=\"0\"\n        cellpadding=\"0\"\n        cellspacing=\"0\"\n        style=\"padding: 0px 5px\"\n        width=\"100%\"\n    >\n      <tbody>\n      <tr>\n        <td>\n          <div\n              style=\"\n                height: 0;\n                display: inline-block;\n                width: 80px;\n                background: none;\n                padding: 0px;\n                margin-top: 15px;\n                margin-bottom: 25px;\n                border-top-width: 2px;\n                border-top-style: solid;\n                border-top-color: #f0f0f0;\n              \"\n          ></div>\n        </td>\n      </tr>\n      </tbody>\n    </table>\n  </div>\n  <div\n      style=\"padding: 0px 15px; font-size: 14px; color: #2b2d36;\"\n      width=\"100%\"\n  >\n    <div style=\"text-align: center\">\n          <span\n              style=\"color: rgb(43, 45, 54); font-weight: bold; font-size: 20px\"\n          >두둥 서비스</span\n          ><span style=\"color: rgb(43, 45, 54); font-size: 20px\"\n    >를 소개합니다&nbsp;😀</span\n    ><br />\n    </div>\n  </div>\n  <table\n      border=\"0\"\n      cellpadding=\"0\"\n      cellspacing=\"0\"\n      style=\"\n          border-collapse: collapse;\n          mso-table-lspace: 0pt;\n          mso-table-rspace: 0pt;\n          -ms-text-size-adjust: 100%;\n          -webkit-text-size-adjust: 100%;\n          table-layout: fixed;\n          margin-top: 15px;\n          margin-bottom: 15px;\n        \"\n      width=\"100%\"\n  >\n    <tbody>\n    <tr>\n      <td\n          style=\"\n                text-align: center;\n                overflow: hidden;\n                clear: both;\n                padding: 0 5px;\n                font-size: 0;\n              \"\n      >\n        <div\n            style=\"\n                  width: 305px;\n                  margin: 0px;\n                  vertical-align: top;\n                  border-collapse: collapse;\n                  padding: 0px 0px 0px 0px;\n                  display: inline-block;\n                \"\n        >\n          <div\n              style=\"\n                    padding: 0 10px;\n                    text-align: justify;\n                    margin: 0px;\n                    width: 100%;\n                    box-sizing: border-box;\n                  \"\n          >\n            <a\n                href=\"https://dudoong.com\"\n                target=\"_blank\"\n                style=\"display: inline\"\n                rel=\"noreferrer noopener\"\n            ><img\n                src=\"https://asset.dudoong.com/common/register-email-card-1.png\"\n                style=\"\n                        display: inline;\n                        vertical-align: middle;\n                        max-width: 100%;\n                        border-width: 0px;\n                        border-image: initial;\n                        text-align: justify;\n                      \"\n                width=\"285\"\n                loading=\"lazy\"\n            /></a>\n          </div>\n          <div\n              style=\"\n                    padding: 5px 10px;\n                    text-align: left;\n                    margin: 0px;\n                    mso-line-height-rule: exactly;\n                    line-height: 1.8;\n                    -ms-text-size-adjust: 100%;\n                    -webkit-text-size-adjust: 100%;\n                    word-break: break-word;\n                    font-size: 14px;\n                  \"\n          >\n            <div style=\"text-align: center\">\n                    <span style=\"color: rgb(82, 84, 99)\"\n                    >내가 속한 그룹을 </span\n                    ><span style=\"font-size: 12px\">&nbsp;</span\n            ><span style=\"color: rgb(43, 45, 54); font-weight: bold\"\n            >호스트</span\n            ><span style=\"color: rgb(82, 84, 99)\"\n              >로 만들어 보세요! </span\n              ><br />\n            </div>\n          </div>\n        </div>\n        <div\n            style=\"\n                  width: 305px;\n                  margin: 0px;\n                  vertical-align: top;\n                  border-collapse: collapse;\n                  padding: 0px 0px 0px 0px;\n                  display: inline-block;\n                \"\n        >\n          <div\n              style=\"\n                    padding: 0 10px;\n                    text-align: justify;\n                    margin: 0px;\n                    width: 100%;\n                    box-sizing: border-box;\n                  \"\n          >\n            <a\n                href=\"https://dudoong.com\"\n                target=\"_blank\"\n                style=\"display: inline\"\n                rel=\"noreferrer noopener\"\n            ><img\n                src=\"https://asset.dudoong.com/common/register-email-card-2.png\"\n                style=\"\n                        display: inline;\n                        vertical-align: middle;\n                        max-width: 100%;\n                        border-width: 0px;\n                        border-image: initial;\n                        text-align: justify;\n                      \"\n                width=\"285\"\n                loading=\"lazy\"\n            /></a>\n          </div>\n          <div\n              style=\"\n                    padding: 5px 10px;\n                    text-align: left;\n                    margin: 0px;\n                    mso-line-height-rule: exactly;\n                    line-height: 1.8;\n                    -ms-text-size-adjust: 100%;\n                    -webkit-text-size-adjust: 100%;\n                    word-break: break-word;\n                    font-size: 14px;\n                  \"\n          >\n            <div style=\"text-align: center\">\n                    <span style=\"color: rgb(43, 45, 54); font-weight: bold\"\n                    >이벤트</span\n                    ><span style=\"color: rgb(82, 84, 99)\"\n              >를 열어 공연을 홍보 할 수 있어요<span\n                  style=\"font-size: 12px\"\n              >&nbsp;</span\n              ></span\n              ><br />\n            </div>\n          </div>\n        </div>\n      </td>\n    </tr>\n    </tbody>\n  </table>\n  <table\n      border=\"0\"\n      cellpadding=\"0\"\n      cellspacing=\"0\"\n      style=\"\n          border-collapse: collapse;\n          mso-table-lspace: 0pt;\n          mso-table-rspace: 0pt;\n          -ms-text-size-adjust: 100%;\n          -webkit-text-size-adjust: 100%;\n          table-layout: fixed;\n          margin-bottom: 15px;\n        \"\n      width=\"100%\"\n  >\n    <tbody>\n    <tr>\n      <td\n          style=\"\n                text-align: center;\n                overflow: hidden;\n                clear: both;\n                padding: 0 5px;\n                font-size: 0;\n              \"\n      >\n        <div\n            style=\"\n                  width: 305px;\n                  margin: 0px;\n                  vertical-align: top;\n                  border-collapse: collapse;\n                  padding: 0px 0px 0px 0px;\n                  display: inline-block;\n                \"\n        >\n          <div\n              style=\"\n                    padding: 0 10px;\n                    text-align: justify;\n                    margin: 0px;\n                    width: 100%;\n                    box-sizing: border-box;\n                  \"\n          >\n            <a\n                href=\"https://dudoong.com\"\n                target=\"_blank\"\n                style=\"display: inline\"\n                rel=\"noreferrer noopener\"\n            ><img\n                src=\"https://asset.dudoong.com/common/register-email-card-3.png\"\n                style=\"\n                        display: inline;\n                        vertical-align: middle;\n                        max-width: 100%;\n                        border-width: 0px;\n                        border-image: initial;\n                        text-align: justify;\n                      \"\n                width=\"285\"\n                loading=\"lazy\"\n            /></a>\n          </div>\n          <div\n              style=\"\n                    padding: 5px 10px;\n                    text-align: left;\n                    margin: 0px;\n                    mso-line-height-rule: exactly;\n                    line-height: 1.8;\n                    -ms-text-size-adjust: 100%;\n                    -webkit-text-size-adjust: 100%;\n                    word-break: break-word;\n                    font-size: 14px;\n                  \"\n          >\n            <div style=\"text-align: center\">\n                    <span style=\"color: rgb(43, 45, 54); font-weight: bold\"\n                    >티켓</span\n                    ><span style=\"color: rgb(82, 84, 99)\"\n              >을 만들어 판매하세요!<span style=\"font-size: 12px\"\n              ></span\n              ></span\n              ><br />\n            </div>\n          </div>\n        </div>\n        <div\n            style=\"\n                  width: 305px;\n                  margin: 0px;\n                  vertical-align: top;\n                  border-collapse: collapse;\n                  padding: 0px 0px 0px 0px;\n                  display: inline-block;\n                \"\n        >\n          <div\n              style=\"\n                    padding: 0 10px;\n                    text-align: justify;\n                    margin: 0px;\n                    width: 100%;\n                    box-sizing: border-box;\n                  \"\n          >\n            <a\n                href=\"https://dudoong.com\"\n                target=\"_blank\"\n                style=\"display: inline\"\n                rel=\"noreferrer noopener\"\n            ><img\n                src=\"https://asset.dudoong.com/common/register-email-card-4.png\"\n                style=\"\n                        display: inline;\n                        vertical-align: middle;\n                        max-width: 100%;\n                        border-width: 0px;\n                        border-image: initial;\n                        text-align: justify;\n                      \"\n                width=\"285\"\n                loading=\"lazy\"\n            /></a>\n          </div>\n          <div\n              style=\"\n                    padding: 5px 10px;\n                    text-align: left;\n                    margin: 0px;\n                    mso-line-height-rule: exactly;\n                    line-height: 1.8;\n                    -ms-text-size-adjust: 100%;\n                    -webkit-text-size-adjust: 100%;\n                    word-break: break-word;\n                    font-size: 14px;\n                  \"\n          >\n            <div style=\"text-align: center\">\n                    <span style=\"color: rgb(43, 45, 54); font-weight: bold\"\n                    >옵션</span><span style=\"color: rgb(82, 84, 99)\"\n              >을 만들어 설문을 받아보세요<span style=\"font-size: 12px\"\n              >&nbsp;</span\n              ></span\n              ><br />\n            </div>\n          </div>\n        </div>\n      </td>\n    </tr>\n    </tbody>\n  </table>\n  <table\n      border=\"0\"\n      cellpadding=\"0\"\n      cellspacing=\"0\"\n      style=\"\n          border-collapse: collapse;\n          mso-table-lspace: 0pt;\n          mso-table-rspace: 0pt;\n          -ms-text-size-adjust: 100%;\n          -webkit-text-size-adjust: 100%;\n          table-layout: fixed;\n\n        \"\n      width=\"100%\"\n  >\n    <tbody>\n    <tr>\n      <td\n          style=\"\n                text-align: center;\n                overflow: hidden;\n                clear: both;\n                padding: 0 5px;\n                font-size: 0;\n              \"\n      >\n        <div\n            style=\"\n                  width: 305px;\n                  margin: 0px;\n                  vertical-align: top;\n                  border-collapse: collapse;\n                  padding: 0px 0px 0px 0px;\n                  display: inline-block;\n                \"\n        >\n          <div\n              style=\"\n                    padding: 0 10px;\n                    text-align: justify;\n                    margin: 0px;\n                    width: 100%;\n                    box-sizing: border-box;\n                  \"\n          >\n            <a\n                href=\"https://dudoong.com\"\n                target=\"_blank\"\n                style=\"display: inline\"\n                rel=\"noreferrer noopener\"\n            ><img\n                src=\"https://asset.dudoong.com/common/register-email-card-5.png\"\n                style=\"\n                        display: inline;\n                        vertical-align: middle;\n                        max-width: 100%;\n                        border-width: 0px;\n                        border-image: initial;\n                        text-align: justify;\n                      \"\n                width=\"285\"\n                loading=\"lazy\"\n            /></a>\n          </div>\n          <div\n              style=\"\n                    padding: 5px 10px;\n                    text-align: left;\n                    margin: 0px;\n                    mso-line-height-rule: exactly;\n                    line-height: 1.8;\n                    -ms-text-size-adjust: 100%;\n                    -webkit-text-size-adjust: 100%;\n                    word-break: break-word;\n                    font-size: 14px;\n                  \"\n          >\n            <div style=\"text-align: center\">\n                    <span style=\"color: rgb(82, 84, 99)\">예약자 </span\n                    ><span style=\"color: rgb(43, 45, 54); font-weight: bold\"\n            >명단</span\n            ><span style=\"color: rgb(82, 84, 99)\"\n            >을 손쉽게 조회할 수 있어요\n                    </span>\n              <br />\n            </div>\n          </div>\n        </div>\n\n        <div\n            style=\"\n                  width: 305px;\n                  margin: 0px;\n                  vertical-align: top;\n                  border-collapse: collapse;\n                  padding: 0px 0px 0px 0px;\n                  display: inline-block;\n                \"\n        >\n          <div\n              style=\"\n                    padding: 0 10px;\n                    text-align: justify;\n                    margin: 0px;\n                    width: 100%;\n                    box-sizing: border-box;\n                  \"\n          >\n            <a\n                href=\"https://dudoong.com\"\n                target=\"_blank\"\n                style=\"display: inline\"\n                rel=\"noreferrer noopener\"\n            ><img\n                src=\"https://asset.dudoong.com/common/register-email-card-6.png\"\n                style=\"\n                        display: inline;\n                        vertical-align: middle;\n                        max-width: 100%;\n                        border-width: 0px;\n                        border-image: initial;\n                        text-align: justify;\n                      \"\n                width=\"285\"\n                loading=\"lazy\"\n            /></a>\n          </div>\n          <div\n              style=\"\n                    padding: 5px 10px;\n                    text-align: left;\n                    margin: 0px;\n                    mso-line-height-rule: exactly;\n                    line-height: 1.8;\n                    -ms-text-size-adjust: 100%;\n                    -webkit-text-size-adjust: 100%;\n                    word-break: break-word;\n                    font-size: 14px;\n                  \"\n          >\n            <div style=\"text-align: center\">\n                    <span style=\"color: rgb(43, 45, 54); font-weight: bold\"\n                    >QR티켓</span\n                    >\n              <span style=\"color: rgb(82, 84, 99)\"\n              >으로 입장을 체크해요</span\n              >\n              <br />\n            </div>\n          </div>\n        </div>\n      </td>\n    </tr>\n    </tbody>\n  </table>\n  <div th:replace=\"fragments/divider :: divider\">divider</div>\n  <div\n      style=\"font-size: 14px; color: #2b2d36; padding: 50px 15px 0px;\"\n      width=\"100%\"\n  >\n    <div style=\"text-align: center;\">\n      <img\n          src=\"https://asset.dudoong.com/common/register-email-bottom.png\"\n          style=\"\n                        display: inline;\n                        vertical-align: middle;\n                        max-width: 100%;\n                        border-width: 0px;\n                        border-image: initial;\n                        text-align: justify;\n                      \"\n          width=\"550\"\n          loading=\"lazy\"\n      />\n    <div>\n\n    <div style=\"text-align: center; padding-top: 50px\">\n\n          <span\n              style=\"color: rgb(43, 45, 54); font-weight: bold; font-size: 20px\"\n          >지금 바로 공연을 만들고 관객들을 모집해보세요!</span\n          >\n    </div>\n  </div>\n  <div\n      th:replace=\"fragments/button :: button(href='https://dudoong.com' , text='이벤트 만들기')\"\n  >\n    button\n  </div>\n</div>\n  </div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/test/java/band/gosrock/infrastructure/InfraIntegrateProfileResolver.java",
    "content": "package band.gosrock.infrastructure;\n\n\nimport org.springframework.test.context.ActiveProfilesResolver;\n\n/**\n * activeProfile 의 Resolver 를 지정 통합테스트에 필요한 properties 인 common, infrastructure , domain 을 지정하기 위함.\n */\npublic class InfraIntegrateProfileResolver implements ActiveProfilesResolver {\n\n    @Override\n    public String[] resolve(Class<?> testClass) {\n        // some code to find out your active profiles\n        return new String[] {\"common\", \"infrastructure\"};\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/test/java/band/gosrock/infrastructure/InfraIntegrateSpringBootTest.java",
    "content": "package band.gosrock.infrastructure;\n\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n/** 도메인 모듈의 통합테스트의 편의성을 위해서 만든 어노테이션 -이찬진 */\n@Target({ElementType.TYPE})\n@Retention(RetentionPolicy.RUNTIME)\n@SpringBootTest(classes = InfraIntegrateTestConfig.class)\n@Documented\npublic @interface InfraIntegrateSpringBootTest {}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/test/java/band/gosrock/infrastructure/InfraIntegrateTestConfig.java",
    "content": "package band.gosrock.infrastructure;\n\n\nimport band.gosrock.common.DuDoongCommonApplication;\nimport org.springframework.context.annotation.ComponentScan;\nimport org.springframework.context.annotation.Configuration;\n\n/** 스프링 부트 설정의 컴포넌트 스캔범위를 지정 통합 테스트를 위함 */\n@Configuration\n@ComponentScan(basePackageClasses = {DuDoongInfraApplication.class, DuDoongCommonApplication.class})\npublic class InfraIntegrateTestConfig {}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/test/java/band/gosrock/infrastructure/config/redis/AutoConfigureTestFeign.java",
    "content": "package band.gosrock.infrastructure.config.redis;\n\n\nimport org.springframework.boot.autoconfigure.ImportAutoConfiguration;\nimport org.springframework.boot.test.autoconfigure.json.AutoConfigureJson;\nimport org.springframework.cloud.openfeign.FeignAutoConfiguration;\n\n@ImportAutoConfiguration(value = FeignAutoConfiguration.class)\n@AutoConfigureJson\npublic @interface AutoConfigureTestFeign {}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/test/java/band/gosrock/infrastructure/config/s3/S3UploadPresignedUrlServiceTest.java",
    "content": "package band.gosrock.infrastructure.config.s3;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n// @SpringBootTest(classes = {S3Config.class, S3UploadPresignedUrlService.class})\n// @ActiveProfiles(\"infrastructure\")\n// class S3UploadPresignedUrlServiceTest {\n//\n//    @Autowired\n//    S3UploadPresignedUrlService s3UploadPresignedUrlService;\n//\n//    @Test\n//    public void S3_url_발급테스트() {\n//        String url = s3UploadPresignedUrlService.execute(1L,\"jpg\");\n//    }\n// }\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/test/java/band/gosrock/infrastructure/outer/api/tossPayments/client/PaymentsCancelClientTest.java",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.client;\n\nimport static com.github.tomakehurst.wiremock.client.WireMock.aResponse;\nimport static com.github.tomakehurst.wiremock.client.WireMock.post;\nimport static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;\nimport static com.github.tomakehurst.wiremock.client.WireMock.stubFor;\nimport static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;\nimport static com.github.tomakehurst.wiremock.client.WireMock.verify;\nimport static org.assertj.core.api.AssertionsForClassTypes.assertThat;\nimport static org.assertj.core.api.AssertionsForClassTypes.catchThrowable;\n\nimport band.gosrock.infrastructure.InfraIntegrateProfileResolver;\nimport band.gosrock.infrastructure.InfraIntegrateTestConfig;\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.request.CancelPaymentsRequest;\nimport feign.RetryableException;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.cloud.contract.spec.internal.HttpStatus;\nimport org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;\nimport org.springframework.test.context.ActiveProfiles;\nimport org.springframework.test.context.ContextConfiguration;\nimport org.springframework.test.context.TestPropertySource;\n\n@ContextConfiguration\n@SpringBootTest(\n        webEnvironment = SpringBootTest.WebEnvironment.NONE,\n        classes = InfraIntegrateTestConfig.class)\n@AutoConfigureWireMock(port = 0)\n@ActiveProfiles(resolver = InfraIntegrateProfileResolver.class)\n@TestPropertySource(properties = {\"feign.toss.url=http://localhost:${wiremock.server.port}\"})\nclass PaymentsCancelClientTest {\n    private static final String IDEMPOTENCY_KEY = \"idempotency-key\";\n    private static final String PAYMENT_KEY = \"1234\";\n    private static final CancelPaymentsRequest REQUEST =\n            new CancelPaymentsRequest(\"test\");\n    @Autowired private PaymentsCancelClient paymentsCancelClient;\n\n    @Test\n    public void 주문취소_실패시_멱등성_테스트() {\n        final String URL = \"/v1/payments/\" + PAYMENT_KEY + \"/cancel\";\n        // given\n        stubFor(\n                post(urlEqualTo(URL))\n                        .willReturn(aResponse().withStatus(HttpStatus.SERVICE_UNAVAILABLE)));\n\n        // when\n        Throwable exception =\n                catchThrowable(\n                        () -> paymentsCancelClient.execute(IDEMPOTENCY_KEY, PAYMENT_KEY, REQUEST));\n\n        // then\n        assertThat(exception).isInstanceOf(RetryableException.class);\n        verify(3, postRequestedFor(urlEqualTo(URL)));\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/test/java/band/gosrock/infrastructure/outer/api/tossPayments/client/TossPaymentsClientTest.java",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.client;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport band.gosrock.common.DuDoongCommonApplication;\nimport band.gosrock.infrastructure.config.feign.FeignCommonConfig;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.ActiveProfiles;\n\n// @AutoConfigureTestFeign\n@ActiveProfiles({\"common\"})\n@SpringBootTest(classes = {FeignCommonConfig.class, DuDoongCommonApplication.class})\nclass TossPaymentsClientTest {\n    //        @Autowired\n    //        PaymentsCancelClient paymentsCancelClient;\n    //        @Autowired PaymentsCreateClient paymentsCreateClient;\n    //        @Autowired PaymentsConfirmClient paymentsConfirmClient;\n    //        @Autowired TransactionGetClient transactionGetClient;\n    //\n    //        @Test\n    //        public void 결제_요청_테스트() {\n    //            CreatePaymentsRequest createPaymentsRequest =\n    //                    CreatePaymentsRequest.builder()\n    //                            .successUrl(\"http://localhost:8080/return-url\")\n    //                            .failUrl(\"http://localhost:8080/failurl-url\")\n    //                            .amount(10000L)\n    //                            .orderId(\"abcd1233\")\n    //                            .method(\"카드\")\n    //                            .orderName(\"주문1\")\n    //                            .build();\n    //\n    //            PaymentsResponse createPaymentResponse =\n    //                    paymentsCreateClient.execute(createPaymentsRequest);\n    //            LocalDateTime localDateTime =\n    // createPaymentResponse.getRequestedAt().toLocalDateTime();\n    //        }\n    //\n    //        @Test\n    //        public void 결제_승인_테스트() {\n    //            ConfirmPaymentsRequest confirmPaymentsRequest =\n    //                    ConfirmPaymentsRequest.builder()\n    //                            .paymentKey(\"qKl56WYb7w4vZnjEJeQVxYQd9zBz9rPmOoBN0k12dzgRG9px\")\n    //                            .amount(10000L)\n    //                            .orderId(\"abcd1233\")\n    //                            .build();\n    //            PaymentsResponse confirmPaymentResponse =\n    //                    paymentsConfirmClient.execute(confirmPaymentsRequest);\n    //\n    //            String orderId = confirmPaymentResponse.getOrderId();\n    //            String paymentKey = confirmPaymentResponse.getPaymentKey();\n    //        }\n\n    //    @Test\n    //    public void 결제_취소_테스트() {\n    //        CancelPaymentsRequest cancelPaymentsRequest =\n    //                CancelPaymentsRequest.builder().cancelReason(\"취소 사유\").build();\n    //\n    //        PaymentsResponse paymentsResponse =\n    //                paymentsCancelClient.execute(\n    //                        \"qKl56WYb7w4vZnjEJeQVxYQd9zBz9rPmOoBN0k12dzgRG9px\",\n    // cancelPaymentsRequest);\n    //    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/test/java/band/gosrock/infrastructure/outer/api/tossPayments/client/TossSettlementClientTest.java",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.client;\n\nimport static com.github.tomakehurst.wiremock.client.WireMock.aResponse;\nimport static com.github.tomakehurst.wiremock.client.WireMock.get;\nimport static com.github.tomakehurst.wiremock.client.WireMock.stubFor;\nimport static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport band.gosrock.infrastructure.InfraIntegrateProfileResolver;\nimport band.gosrock.infrastructure.InfraIntegrateTestConfig;\nimport band.gosrock.infrastructure.outer.api.tossPayments.dto.response.SettlementResponse;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.LocalDate;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.test.context.ActiveProfiles;\nimport org.springframework.test.context.ContextConfiguration;\nimport org.springframework.test.context.TestPropertySource;\nimport org.springframework.util.ResourceUtils;\n\n@ContextConfiguration\n@SpringBootTest(\n        webEnvironment = SpringBootTest.WebEnvironment.NONE,\n        classes = InfraIntegrateTestConfig.class)\n@AutoConfigureWireMock(port = 0)\n@ActiveProfiles(resolver = InfraIntegrateProfileResolver.class)\n@TestPropertySource(\n        properties = {\n            \"feign.toss.url=http://localhost:${wiremock.server.port}\",\n            \"spring.thymeleaf.enabled=false\"\n        })\npublic class TossSettlementClientTest {\n\n    @Autowired private SettlementClient settlementClient;\n\n    @Test\n    public void 정산요청_올바르게_파싱되어야한다() throws IOException {\n        Path file = ResourceUtils.getFile(\"classpath:payload/settlement-response.json\").toPath();\n\n        stubFor(\n                get(urlPathEqualTo(\"/v1/settlements\"))\n                        .willReturn(\n                                aResponse()\n                                        .withStatus(HttpStatus.OK.value())\n                                        .withHeader(\n                                                \"Content-Type\", MediaType.APPLICATION_JSON_VALUE)\n                                        .withBody(Files.readAllBytes(file))));\n        LocalDate now = LocalDate.now();\n        List<SettlementResponse> test = settlementClient.execute(now, now, \"test\", 1, 10);\n\n        SettlementResponse settlementResponse = test.get(0);\n        assertEquals(settlementResponse.getFees().size(), 2);\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/test/java/band/gosrock/infrastructure/outer/api/tossPayments/config/PaymentConfirmErrorDecoderTest.java",
    "content": "package band.gosrock.infrastructure.outer.api.tossPayments.config;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport band.gosrock.infrastructure.outer.api.tossPayments.exception.PaymentsConfirmErrorCode;\nimport org.junit.jupiter.api.Test;\n\nclass PaymentConfirmErrorDecoderTest {\n\n    @Test\n    void 코드가주어졌을때_ErrorCode_로변환할수있어야한다() {\n        String code = \"ALREADY_PROCESSED_PAYMENT\";\n        PaymentsConfirmErrorCode paymentsConfirmErrorCode = PaymentsConfirmErrorCode.valueOf(code);\n\n        assertEquals(code, paymentsConfirmErrorCode.name());\n    }\n}\n"
  },
  {
    "path": "DuDoong-Infrastructure/src/test/resources/application.yml",
    "content": "# test 용 키들\naws:\n  access-key: ${AWS_ACCESS_KEY}\n  secret-key: ${AWS_SECRET_KEY}\n  s3:\n    bucket: ${AWS_S3_BUCKET}\n    base-url: ${AWS_S3_BASE_URL}\n\n  redis:\n    host: ${REDIS_HOST:localhost}\n    port: ${REDIS_PORT:6379}\n    password: ${REDIS_PASSWORD:}\n\nlogging:\n  level:\n    band.gosrock.infrastructure : DEBUG"
  },
  {
    "path": "DuDoong-Infrastructure/src/test/resources/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n  <include resource=\"org/springframework/boot/logging/logback/base.xml\" />\n  <logger name=\"org.springframework\" level=\"INFO\"/>\n  <include resource=\"band.gosrock\"/>\n  <logger name=\"band.gosrock\" level=\"DEBUG\"/>\n</configuration>"
  },
  {
    "path": "DuDoong-Infrastructure/src/test/resources/payload/settlement-response.json",
    "content": "[\n  {\n    \"mId\": \"tosspayments\",\n    \"paymentKey\": \"5zJ4xY7m0kODnyRpQWGrN2xqGlNvLrKwv1M9ENjbeoPaZdL6\",\n    \"transactionKey\": \"8B4F646A829571D870A3011A4E13D640\",\n    \"orderId\": \"a4CWyWY5m89PNh7xJwhk1\",\n    \"currency\": \"KRW\",\n    \"method\": \"카드\",\n    \"amount\": 34000,\n    \"interestFee\": 0,\n    \"fees\": [\n      {\n        \"type\": \"BASE\",\n        \"fee\": \"800\"\n      },\n      {\n        \"type\": \"INSTALLMENT\",\n        \"fee\": \"60\"\n      }\n    ],\n    \"supplyAmount\": 782,\n    \"vat\": 78,\n    \"payOutAmount\": 33140,\n    \"approvedAt\": \"2021-01-02T12:30:09+09:00\",\n    \"soldDate\": \"2021-01-02\",\n    \"paidOutDate\": \"2021-01-04\"\n  }\n]"
  },
  {
    "path": "LICENSE",
    "content": "GNU AFFERO GENERAL PUBLIC LICENSE\nVersion 3, 19 November 2007\n\nCopyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n\nEveryone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.\n\n                            Preamble\n\nThe GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.\n\nThe licenses for most software and other practical works are designed to take away your freedom to share and change the works.  By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users.\n\nWhen we speak of free software, we are referring to freedom, not price.  Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.\n\nDevelopers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software.\n\nA secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate.  Many developers of free software are heartened and encouraged by the resulting cooperation.  However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public.\n\nThe GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community.  It requires the operator of a network server to provide the source code of the modified version running there to the users of that server.  Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version.\n\nAn older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals.  This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license.\n\nThe precise terms and conditions for copying, distribution and modification follow.\n\n                       TERMS AND CONDITIONS\n\n0. Definitions.\n\n\"This License\" refers to version 3 of the GNU Affero General Public License.\n\n\"Copyright\" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.\n\n\"The Program\" refers to any copyrightable work licensed under this License.  Each licensee is addressed as \"you\".  \"Licensees\" and \"recipients\" may be individuals or organizations.\n\nTo \"modify\" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy.  The resulting work is called a \"modified version\" of the earlier work or a work \"based on\" the earlier work.\n\nA \"covered work\" means either the unmodified Program or a work based on the Program.\n\nTo \"propagate\" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy.  Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.\n\nTo \"convey\" a work means any kind of propagation that enables other parties to make or receive copies.  Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.\n\nAn interactive user interface displays \"Appropriate Legal Notices\" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License.  If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.\n\n1. Source Code.\nThe \"source code\" for a work means the preferred form of the work for making modifications to it.  \"Object code\" means any non-source form of a work.\n\nA \"Standard Interface\" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.\n\nThe \"System Libraries\" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form.  A \"Major Component\", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.\n\nThe \"Corresponding Source\" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities.  However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work.  For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\nThe Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.\n\nThe Corresponding Source for a work in source code form is that same work.\n\n2. Basic Permissions.\nAll rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met.  This License explicitly affirms your unlimited permission to run the unmodified Program.  The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work.  This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.\n\nYou may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force.  You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright.  Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.\n\nConveying under any other circumstances is permitted solely under the conditions stated below.  Sublicensing is not allowed; section 10 makes it unnecessary.\n\n3. Protecting Users' Legal Rights From Anti-Circumvention Law.\nNo covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.\n\nWhen you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.\n\n4. Conveying Verbatim Copies.\nYou may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.\n\nYou may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.\n\n5. Conveying Modified Source Versions.\nYou may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7.  This requirement modifies the requirement in section 4 to \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy.  This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged.  This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.\n\nA compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an \"aggregate\" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit.  Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.\n\n6. Conveying Non-Source Forms.\nYou may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source.  This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.\n\n    d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge.  You need not require recipients to copy the Corresponding Source along with the object code.  If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source.  Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.\n\nA separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.\n\nA \"User Product\" is either (1) a \"consumer product\", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling.  In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage.  For a particular product received by a particular user, \"normally used\" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product.  A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.\n\n\"Installation Information\" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source.  The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.\n\nIf you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information.  But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).\n\nThe requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed.  Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.\n\nCorresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.\n\n7. Additional Terms.\n\"Additional permissions\" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law.  If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.\n\nWhen you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it.  (Additional permissions may be written to require their own removal in certain cases when you modify the work.)  You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.\n\nNotwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.\n\nAll other non-permissive additional terms are considered \"further restrictions\" within the meaning of section 10.  If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term.  If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.\n\nIf you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.\n\nAdditional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.\n\n8. Termination.\n\nYou may not propagate or modify a covered work except as expressly provided under this License.  Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).\n\nHowever, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.\n\nMoreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.\n\nTermination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License.  If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.\n\n9. Acceptance Not Required for Having Copies.\n\nYou are not required to accept this License in order to receive or run a copy of the Program.  Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance.  However, nothing other than this License grants you permission to propagate or modify any covered work.  These actions infringe copyright if you do not accept this License.  Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.\n\n10. Automatic Licensing of Downstream Recipients.\n\nEach time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License.  You are not responsible for enforcing compliance by third parties with this License.\n\nAn \"entity transaction\" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations.  If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.\n\nYou may not impose any further restrictions on the exercise of the rights granted or affirmed under this License.  For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.\n\n11. Patents.\n\nA \"contributor\" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based.  The work thus licensed is called the contributor's \"contributor version\".\n\nA contributor's \"essential patent claims\" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version.  For purposes of this definition, \"control\" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.\n\nEach contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.\n\nIn the following three paragraphs, a \"patent license\" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement).  To \"grant\" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.\n\nIf you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.\n\nIf, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.\n\nA patent license is \"discriminatory\" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License.  You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.\n\nNothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.\n\n12. No Surrender of Others' Freedom.\n\nIf conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License.  If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.\n\n13. Remote Network Interaction; Use with the GNU General Public License.\n\nNotwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software.  This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph.\n\nNotwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work.  The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License.\n\n14. Revised Versions of this License.\n\nThe Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time.  Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Program specifies that a certain numbered version of the GNU Affero General Public License \"or any later version\" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation.  If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation.\n\nIf the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.\n\nLater license versions may give you additional or different permissions.  However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.\n\n15. Disclaimer of Warranty.\n\nTHERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n16. Limitation of Liability.\n\nIN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\n17. Interpretation of Sections 15 and 16.\n\nIf the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.\n\nEND OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\nIf you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program.  It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the \"copyright\" line and a pointer to where the full notice is found.\n\n     <one line to give the program's name and a brief idea of what it does.>\n     Copyright (C) <year>  <name of author>\n\n     This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\n\n     This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.\n\n     You should have received a copy of the GNU Affero General Public License along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source.  For example, if your program is a web application, its interface could display a \"Source\" link that leads users to an archive of the code.  There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements.\n\nYou should also get your employer (if you work as a programmer) or school, if any, to sign a \"copyright disclaimer\" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see <http://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "README.md",
    "content": "\n![Frame 4](https://user-images.githubusercontent.com/55226431/221772740-e9946fda-a24c-4b90-8871-4d1d8a340725.png)\n\n[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Gosrock_DuDoong-Backend&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Gosrock_DuDoong-Backend)\n[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Gosrock_DuDoong-Backend&metric=coverage)](https://sonarcloud.io/summary/new_code?id=Gosrock_DuDoong-Backend)\n[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=Gosrock_DuDoong-Backend&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=Gosrock_DuDoong-Backend)\n<br/>\n\n# 두둥<img src=\"https://user-images.githubusercontent.com/55226431/221770112-27710500-f49a-4c7b-8765-8b3698566e55.png\" align=left width=100>\n\n> 모두를 위한 새로운 공연 라이프, 두둥! • <b>백엔드</b> 레포지토리\n\n<br/><br/>\n\n\n> **두둥은 홍익대학교 컴퓨터 공학과 소속 밴드부 <a href=\"https://github.com/Gosrock\">고스락</a> 에서 만든 서비스에요!**\n\n<br/>\n\n<img width=\"100%\" align=center alt=\"readme\" src=\"https://user-images.githubusercontent.com/55226431/221773192-5e178d8e-93a4-4a50-821f-3dbd9c9ac759.png\">\n\n<br/>\n\n## ✨ 서비스 관련\n- [랜딩페이지](https://dudoong.com)\n- [호스트 관리자 페이지](https://dudoong.com/admin)\n- [서비스 소개 노션](https://dudoong.notion.site/c4999331a2aa47299e1c6821a7dee9af)\n- [Storybook](https://gosrock.github.io/DuDoong-Front)\n<div>\n<img src=\"https://user-images.githubusercontent.com/55226431/221772278-78452025-d9df-4676-90e7-ca6d4033ed7e.gif\"  width=\"100%\" >\n</div>\n\n<br>\n\n## 📚 사용 스택\n<div align=\"left\">\n<div>\n<img src=\"https://img.shields.io/badge/Spring Boot-6DB33F?style=flat-square&logo=Spring Boot&logoColor=white\">\n<img src=\"https://img.shields.io/badge/Gradle-02303A?style=flat-square&logo=Gradle&logoColor=white\">\n</div>\n\n<div>\n<img src=\"https://img.shields.io/badge/MySQL-4479A1.svg?style=flat-square&logo=MySQL&logoColor=white\">\n<img src=\"https://img.shields.io/badge/Redis-DC382D?style=flat-square&logo=Redis&logoColor=white\">\n</div>\n\n<div>\n<img src=\"https://img.shields.io/badge/Amazon AWS-232F3E?style=flat-square&logo=Amazon AWS&logoColor=white\">\n<img src=\"https://img.shields.io/badge/Docker-2496ED?style=flat-square&logo=Docker&logoColor=white\">\n<img src=\"https://img.shields.io/badge/JSON Web Tokens-000000?style=flat-square&logo=JSON Web Tokens&logoColor=white\">\n</div>\n\n<div>\n<img src=\"https://img.shields.io/badge/SonarCloud-F3702A?style=flat-square&logo=SonarCloud&logoColor=white\">\n<img src=\"https://img.shields.io/badge/Amazon CloudWatch-FF4F8B?style=flat-square&logo=Amazon CloudWatch&logoColor=white\">\n<img src=\"https://img.shields.io/badge/Slack-4A154B?style=flat-square&logo=slack&logoColor=white\">\n</div>\n\n</div>\n\n<br/>\n\n\n\n## 🔍 개발 과정\n- [찬진 : Spring disable Aop in test](https://devnm.tistory.com/24)\n- [찬진 : Spring open api swagger basic auth 세팅](https://devnm.tistory.com/25)\n- [찬진 : Spring swagger api 하나만 인증 풀기](https://devnm.tistory.com/26)\n- [찬진 : Spring 에러코드 도메인 별로 분리하기](https://devnm.tistory.com/27)\n- [찬진 : Spring 공통 응답 형식 만들기 ResponseBodyAdvice](https://devnm.tistory.com/28)\n- [찬진 : Spring swagger 같은 응답 코드 여러 에러 예시 만들기](https://devnm.tistory.com/29)\n- [찬진 : spring 프록시 환경에서 HttpContentCache 적용](https://devnm.tistory.com/30)\n- [찬진 : spring rate limit 적용히기 bucket4j](https://devnm.tistory.com/31)\n- [찬진 : spring thymeleaf to pdf 이미지,한글 적용하기](https://devnm.tistory.com/32)\n- [찬진 : spring batch 도커로 세팅하기 with jenkins](https://devnm.tistory.com/33)\n- [찬진 : spring feign client wiremock test](https://devnm.tistory.com/34)\n- [찬진 : spring oauth Open ID Connect with kakao](https://devnm.tistory.com/35)\n- [찬진 : 멀티모듈 jacoco , sonarqube (cloud) 세팅](https://devnm.tistory.com/36)\n- [찬진 : spring redisson 분산락 Aop 적용기](https://devnm.tistory.com/37)\n- [찬진 : 도커 로그 ec2환경에서 클라우드 와치로 전송하기](https://devnm.tistory.com/8)\n- [경민 : Custom Enum Validator 구현하기](https://gengminy.tistory.com/47)\n- [경민 : Reflection 을 이용하여 Enum Validator 개선하기](https://gengminy.tistory.com/48)\n- [경민 : Custom Enum Deserializer 구현하여 Enum 에 없는 값 null 로 파싱하기](https://gengminy.tistory.com/49)\n- [경민 : 스프링 날짜 타입 JSON 변환 및 포맷팅하기 - @JsonFormat, @JacksonAnnotationsInside](https://gengminy.tistory.com/50)\n- [경민 : Incoming WebHooks 로 슬랙봇 생성 및 슬랙 메세지 전송하기](https://gengminy.tistory.com/51)\n\n\n\n\n\n## 📁 Project Structure\nDDD와 멀티모듈 구조를 사용했습니다.\n각 도메인별 연관관계를 최대한 끊어내고\n도메인 이벤트를 활용해 도메인간의 의존성을 줄였습니다.\n```bash\n├── DuDoong-Api  \n│       └── band.gosrock.api  \n│           └── <각 usecase 별 패키지> # ex : order,issuedTicket\n│               └── controller\n│               └── dto\n│               └── mapper # 분산락으로 인한 다른트랜잭션일 때 최신의 정보를 가져오기 위함\n│               └── service # usecase 파사드 형태로 다른 도메인서비스들의 반환값을 모아 응답값 생성\n├── DuDoong-Batch  # 배치 서비스 어플리케이션 ( 젠킨스로 크론잡 )\n├── DuDoong-Common  # 공통으로 쓰이는 어노테이션, 에러 코드등\n├── DuDoong-Domain   \n│       └── band.gosrock.domain     \n│           ├── common  # 분산락 aop , 도메인 이벤트 발행\n│           └── domains \n│               └── <도메인>  # 각도메인 ex : order ,ticket\n│                   └── adaptor # 도메인 리포지토리를 한번 더 감싼 컴포넌트\n│                   └── domain # 도메인 오브젝트\n│                   └── exception # 도메인별 에러 정의\n│                   └── repostiory # 도메인 리포지토리\n│                   └── service # 도메인 서비스, 도메인 이벤트 핸들러\n├── DuDoong-Infrastructure  # 레디스 , feignClient(외부 api 콜) , 메일 ( aws ses ) ,s3 등.\n└── DuDoong-Socket  \n```\n\n\n## 💻 Developers\n<table>\n    <tr align=\"center\">\n        <td><B>Lead•Backend</B></td>\n        <td><B>Backend</B></td>\n        <td><B>Backend</B></td>\n        <td><B>Backend</B></td>\n        <td><B>Backend</B></td>\n    </tr>\n    <tr align=\"center\">\n        <td><B>이찬진</B></td>\n        <td><B>김민준</B></td>\n        <td><B>김원진</B></td>\n        <td><B>노경민</B></td>\n        <td><B>이채린</B></td>\n    </tr>\n    <tr align=\"center\">\n        <td>\n            <img src=\"https://github.com/ImNM.png?size=100\">\n            <br>\n            <a href=\"https://github.com/ImNM\"><I>ImNM</I></a>\n        </td>\n        <td>\n            <img src=\"https://github.com/sanbonai06.png?size=100\" width=\"100\">\n            <br>\n            <a href=\"https://github.com/sanbonai06\"><I>sanbonai06</I></a>\n        </td>\n        <td>\n            <img src=\"https://github.com/kim-wonjin.png?size=100\" width=\"100\">\n            <br>\n            <a href=\"https://github.com/kim-wonjin\"><I>kim-wonjin</I></a>\n        </td>\n        <td>\n            <img src=\"https://github.com/gengminy.png?size=100\" width=\"100\">\n            <br>\n            <a href=\"https://github.com/gengminy\"><I>gengminy</I></a>\n        </td>\n        <td>\n            <img src=\"https://github.com/cofls6581.png?size=100\" width=\"100\">\n            <br>\n            <a href=\"https://github.com/cofls6581\"><I>cofls6581</I></a>\n        </td>\n    </tr>\n</table>"
  },
  {
    "path": "build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.tasks.KotlinCompile\n\nplugins {\n    java\n    id(\"org.springframework.boot\") version \"3.2.0\"\n    id(\"io.spring.dependency-management\") version \"1.1.4\" apply false\n    kotlin(\"jvm\") version \"1.9.22\" apply false\n    kotlin(\"plugin.spring\") version \"1.9.22\" apply false\n    kotlin(\"plugin.jpa\") version \"1.9.22\" apply false\n    kotlin(\"kapt\") version \"1.9.22\" apply false\n    kotlin(\"plugin.allopen\") version \"1.9.22\" apply false\n    id(\"org.jlleitschuh.gradle.ktlint\") version \"11.6.1\"\n}\n\ntasks.bootJar { enabled = false }\n\nrepositories {\n    mavenCentral()\n}\n\nsubprojects {\n    group = \"band.gosrock\"\n    version = \"0.0.1-SNAPSHOT\"\n\n    apply(plugin = \"java\")\n    apply(plugin = \"java-library\")\n    apply(plugin = \"org.springframework.boot\")\n    apply(plugin = \"io.spring.dependency-management\")\n    apply(plugin = \"org.jetbrains.kotlin.jvm\")\n    apply(plugin = \"org.jetbrains.kotlin.plugin.spring\")\n    apply(plugin = \"org.jetbrains.kotlin.plugin.jpa\")\n    apply(plugin = \"org.jetbrains.kotlin.plugin.allopen\")\n    apply(plugin = \"org.jetbrains.kotlin.kapt\")\n\n    java {\n        toolchain {\n            languageVersion.set(JavaLanguageVersion.of(21))\n        }\n    }\n\n    // JPA 엔티티 클래스를 open으로 만들어 Hibernate 프록시 및 Mockito 서브클래스 목킹 지원\n    configure<org.jetbrains.kotlin.allopen.gradle.AllOpenExtension> {\n        annotation(\"jakarta.persistence.Entity\")\n        annotation(\"jakarta.persistence.Embeddable\")\n        annotation(\"jakarta.persistence.MappedSuperclass\")\n    }\n\n    // KAPT adds -proc:none to compileTestJava, breaking Lombok annotation processing\n    tasks.withType<JavaCompile> {\n        if (name == \"compileTestJava\") {\n            doFirst {\n                options.compilerArgs.remove(\"-proc:none\")\n            }\n        }\n    }\n\n    tasks.withType<KotlinCompile> {\n        compilerOptions {\n            freeCompilerArgs.add(\"-Xjsr305=strict\")\n            jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)\n        }\n    }\n\n    repositories {\n        mavenCentral()\n    }\n\n    dependencies {\n        // Kotlin 기본 의존성\n        implementation(\"org.jetbrains.kotlin:kotlin-reflect\")\n        implementation(\"com.fasterxml.jackson.module:jackson-module-kotlin\")\n\n        annotationProcessor(\"org.springframework.boot:spring-boot-configuration-processor\")\n        testImplementation(\"org.springframework.boot:spring-boot-starter-test\")\n\n        // Lombok (Java 테스트 파일에서 사용 - 추후 테스트 Kotlin 전환 시 제거)\n        testCompileOnly(\"org.projectlombok:lombok\")\n        testAnnotationProcessor(\"org.projectlombok:lombok\")\n    }\n\n    tasks.test {\n        useJUnitPlatform()\n    }\n}\n\nktlint {\n    version.set(\"0.50.0\")\n    android.set(false)\n    outputToConsole.set(true)\n    filter {\n        exclude(\"**/generated/**\")\n        exclude(\"**/build/**\")\n    }\n}\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3.7'\nservices:\n  redis:\n    image: 'redis:alpine'\n    ports:\n      - '6379:6379'\n\n  mysql:\n    image: 'mysql:8.4'\n    ports:\n      - '13306:3306'\n    environment:\n      MYSQL_ROOT_PASSWORD: dudoong\n      MYSQL_ROOT_HOST: '%'\n      MYSQL_DATABASE: dudoong\n      MYSQL_USER: dudoong\n      MYSQL_PASSWORD: dudoong\n    volumes:\n      - dudoong-mysql-data:/var/lib/mysql\n    command: >\n      --character-set-server=utf8mb4\n      --collation-server=utf8mb4_unicode_ci\n      --default-time-zone=+09:00\n      --mysql-native-password=ON\n\nvolumes:\n  dudoong-mysql-data:\n"
  },
  {
    "path": "e2e-tests/.gitignore",
    "content": "__pycache__/\n.pytest_cache/\n"
  },
  {
    "path": "e2e-tests/README.md",
    "content": "# DuDoong Backend E2E 테스트 스위트\n\n외부 HTTP 요청으로 실행되는 엔드-투-엔드 테스트 스위트입니다. 로컬 개발 환경에서 백엔드 서버의 모든 주요 기능을 통합으로 검증합니다.\n\n## 개요\n\n### 테스트의 목적\n\n- **실제 HTTP 통신**: Mock이 아닌 실제 API 엔드포인트로 검증\n- **사용자 여정**: 로그인 → 호스트 → 이벤트 → 티켓 → 주문 → 발급 → 환불의 전체 플로우 검증\n- **공유 상태 관리**: `TestState`를 통해 테스트 간 생성된 리소스(host_id, event_id 등)를 추적\n- **외부 API 통합**: Kakao, Toss, Slack 등 외부 API 호출을 로컬 Mock 엔드포인트로 처리\n\n### 왜 외부 HTTP를 사용하는가?\n\n1. **통합 테스트**: 전체 요청/응답 사이클, 직렬화, 에러 처리를 검증\n2. **배포 전 검증**: 실제 서버에서 동작하는 그대로 테스트\n3. **엔드투엔드**: API 계층, 비즈니스 로직, 데이터베이스를 모두 포함한 흐름 검증\n\n---\n\n## 실행 방법\n\n### 1) 서버 실행\n\n로컬 개발 환경에서 백엔드 서버를 먼저 실행합니다.\n\n```bash\n# DuDoong-Backend 디렉토리에서\ncd /Users/chanjin/Desktop/dudoong-v2/DuDoong-Backend\n\n# API 서버 실행 (기본 포트: 8080)\n./gradlew :DuDoong-Api:bootRun\n```\n\n또는 IDE에서 `DuDoong-Api` 모듈의 메인 클래스를 실행합니다.\n\n**서버 준비 확인:**\n```bash\ncurl http://localhost:8080/api/v1/auth/oauth/local/login\n# 서버가 응답하면 테스트 실행 가능\n```\n\n### 2) Python 환경 설정\n\n```bash\n# e2e-tests 디렉토리로 이동\ncd /Users/chanjin/Desktop/dudoong-v2/DuDoong-Backend/e2e-tests\n\n# 필수 패키지 설치\npip install pytest requests\n\n# 또는 requirements.txt가 있으면\npip install -r requirements.txt\n```\n\n### 3) E2E 테스트 실행\n\n**전체 테스트 실행:**\n```bash\n# e2e-tests 디렉토리에서\npytest -v\n\n# 또는 상세한 로그 출력\npytest -v -s\n```\n\n**특정 테스트 파일만 실행:**\n```bash\n# 인증 테스트만\npytest test_01_auth.py -v -s\n\n# 주문 플로우 테스트만\npytest test_05_order_flow.py -v -s\n```\n\n**특정 테스트만 실행:**\n```bash\npytest test_05_order_flow.py::test_create_order -v -s\n```\n\n**커스텀 API URL로 실행:**\n```bash\n# 기본값: http://localhost:8080/api\n# 다른 URL에서 테스트하려면\nAPI_BASE_URL=http://staging.dudoong.com/api pytest -v\n```\n\n### 4) 테스트 실행 순서\n\n`conftest.py`의 `auth_token` 픽스처가 최초 1회 로그인을 수행하므로, 테스트는 다음 순서로 실행됩니다:\n\n```\ntest_01_auth.py\n├── test_local_login (선택사항: 이미 auth_token이 로그인함)\n├── test_token_refresh\n└── test_unauthenticated_access\n\ntest_02_host.py\n├── test_create_host (host_id 저장)\n└── test_read_host_profiles\n\ntest_03_event.py\n├── test_create_event (event_id 저장)\n├── test_update_event_basic\n├── test_update_event_detail\n├── test_search_events\n└── test_read_event\n\ntest_04_ticket_item.py\n├── test_create_free_ticket_item (ticket_item_id 저장)\n├── test_get_event_ticket_items\n└── test_open_event\n\ntest_05_order_flow.py\n├── test_create_cart (cart_id 저장)\n├── test_read_cart\n├── test_create_order (order_uuid 저장)\n├── test_free_order\n└── test_read_order\n\ntest_06_issued_ticket.py\n├── test_read_order_tickets\n└── test_read_my_orders\n\ntest_07_comment.py\n├── test_create_comment (comment_id 저장)\n├── test_read_comments\n└── test_get_comment_counts\n\ntest_08_refund.py\n└── test_refund_order\n```\n\n---\n\n## 테스트 시나리오 맵\n\n### test_01_auth.py - 인증 시나리오\n\n| 테스트 | 설명 | 인증 | 목적 |\n|--------|------|------|------|\n| `test_local_login` | 로컬 개발 로그인 | X | 로그인 후 accessToken/refreshToken 획득 |\n| `test_token_refresh` | 토큰 갱신 | X | refreshToken으로 새로운 accessToken 발급 |\n| `test_unauthenticated_access` | 미인증 접근 차단 | X | 토큰 없이 보호된 엔드포인트 접근 시 401/403 반환 |\n\n### test_02_host.py - 호스트 시나리오\n\n| 테스트 | 설명 | 인증 | 의존 | 생성 리소스 |\n|--------|------|------|------|------------|\n| `test_create_host` | 호스트 생성 | O | - | host_id |\n| `test_read_host_profiles` | 내 호스트 목록 조회 | O | host_id | - |\n\n**핵심 검증:**\n- 호스트 정보 정상 저장\n- 생성된 호스트가 목록에 포함되는지 확인\n\n### test_03_event.py - 이벤트 시나리오\n\n| 테스트 | 설명 | 인증 | 의존 | 생성 리소스 |\n|--------|------|------|------|------------|\n| `test_create_event` | 이벤트 생성 | O | host_id | event_id |\n| `test_update_event_basic` | 기본 정보 수정 | O | event_id | - |\n| `test_update_event_detail` | 상세 정보 수정 | O | event_id | - |\n| `test_search_events` | 이벤트 검색 | X | - | - |\n| `test_read_event` | 이벤트 상세 조회 | X | event_id | - |\n\n**핵심 검증:**\n- 이벤트 생성 및 ID 발급\n- 기본 정보(이름, 시작시각, 장소) 수정\n- 상세 정보(포스터, 설명) 수정\n- 키워드 검색 기능\n- 공개 조회 가능\n\n### test_04_ticket_item.py - 티켓 상품 시나리오\n\n| 테스트 | 설명 | 인증 | 의존 | 생성 리소스 |\n|--------|------|------|------|------------|\n| `test_create_free_ticket_item` | 무료 티켓 생성 | O | event_id | ticket_item_id |\n| `test_get_event_ticket_items` | 티켓 상품 목록 조회 | X | event_id | - |\n| `test_open_event` | 이벤트 오픈 | O | event_id, ticket_item_id | - |\n\n**핵심 검증:**\n- payType: 무료티켓, approveType: 선착순으로 생성\n- 생성된 티켓이 목록에 포함되는지 확인\n- 기본 정보 + 상세 정보 + 티켓 상품 모두 존재할 때만 오픈 가능\n\n### test_05_order_flow.py - 주문 플로우 (핵심 시나리오)\n\n| 테스트 | 설명 | 인증 | 의존 | 생성 리소스 |\n|--------|------|------|------|------------|\n| `test_create_cart` | 장바구니 생성 | O | ticket_item_id | cart_id |\n| `test_read_cart` | 장바구니 조회 | O | - | - |\n| `test_create_order` | 주문 생성 | O | cart_id | order_uuid |\n| `test_free_order` | 무료 주문 완료 | O | order_uuid | - |\n| `test_read_order` | 주문 상세 조회 | O | order_uuid | - |\n\n**핵심 검증:**\n- 티켓 상품을 장바구니에 추가\n- 장바구니를 주문서로 변환\n- 무료 주문 완료(0원 결제)\n- 주문 조회 시 발급 티켓 포함 확인\n\n### test_06_issued_ticket.py - 발급 티켓 시나리오\n\n| 테스트 | 설명 | 인증 | 의존 | 목적 |\n|--------|------|------|------|------|\n| `test_read_order_tickets` | 주문 내 티켓 목록 조회 | O | order_uuid | 발급된 티켓 상세 정보 확인 |\n| `test_read_my_orders` | 마이페이지 예매 목록 | O | order_uuid | 현재 유효한 예매 목록 조회 |\n\n**핵심 검증:**\n- 주문 완료 후 티켓이 정상적으로 발급되었는지 확인\n- 마이페이지에서 예매 목록 조회(showing=true)\n\n### test_07_comment.py - 응원톡(댓글) 시나리오\n\n| 테스트 | 설명 | 인증 | 의존 | 생성 리소스 |\n|--------|------|------|------|------------|\n| `test_create_comment` | 응원글 생성 | O | event_id | comment_id |\n| `test_read_comments` | 응원글 목록 조회 | X | event_id | - |\n| `test_get_comment_counts` | 응원글 개수 조회 | X | event_id | - |\n\n**핵심 검증:**\n- nickName 1~10자, content 1~150자 제약 확인\n- 생성된 응원글이 목록에 포함\n- 응원글 개수 카운트\n\n### test_08_refund.py - 환불 시나리오\n\n| 테스트 | 설명 | 인증 | 의존 | 생성 리소스 |\n|--------|------|------|------|------------|\n| `test_refund_order` | 주문 환불 | O | ticket_item_id | refund_order_uuid |\n\n**핵심 검증:**\n- 새로운 주문 생성 → 무료 결제 → 환불 요청\n- 환불 후 orderId 반환 확인\n- 기존 order_uuid와 독립적으로 처리\n\n---\n\n## 테스트 커버리지 매트릭스\n\n### 기능별 커버리지\n\n| 기능 | 테스트 | 상태 |\n|------|--------|------|\n| **인증** | 로그인, 토큰 갱신, 미인증 접근 | ✓ 완료 |\n| **호스트** | 생성, 목록 조회, 초대/가입/거절, 역할변경, Slack URL, 권한검증 | ✓ 완료 |\n| **이벤트** | 생성, 기본/상세 수정, 검색, 상세 조회, 오픈 | ✓ 완료 |\n| **티켓 상품** | 생성(무료/두둥), 목록 조회, 옵션 CRUD, 삭제, 재고 검증 | ✓ 완료 |\n| **장바구니** | 생성, 조회 | ✓ 완료 |\n| **주문** | 생성, 무료 결제, 상세 조회, 승인/거절, 취소, 쿠폰 적용 | ✓ 완료 |\n| **발급 티켓** | 목록 조회, 예매 목록 조회 | ✓ 완료 |\n| **응원톡** | 생성, 목록 조회, 개수 조회 | ✓ 완료 |\n| **환불** | 환불 요청, 이중 환불 방지, 재고 복원, 티켓 취소 상태 | ✓ 완료 |\n| **쿠폰** | 캠페인 생성, 발급, 적용 주문, 목록 조회 | ✓ 완료 |\n| **관리자** | 주문/티켓 테이블 조회, 입장 처리, 이중 입장 차단 | ✓ 완료 |\n| **엣지 케이스** | 재고 소진, 타유저 조회 차단, 중복 결제, 미인증 | ✓ 완료 |\n\n### API 엔드포인트 커버리지\n\n| 엔드포인트 | 메서드 | 테스트 | 상태 |\n|-----------|--------|--------|------|\n| `/v1/auth/oauth/local/login` | POST | test_local_login | ✓ |\n| `/v1/auth/token/refresh` | POST | test_token_refresh | ✓ |\n| `/v1/hosts` | POST | test_create_host | ✓ |\n| `/v1/hosts` | GET | test_read_host_profiles | ✓ |\n| `/v1/events` | POST | test_create_event | ✓ |\n| `/v1/events/{eventId}/basic` | PATCH | test_update_event_basic | ✓ |\n| `/v1/events/{eventId}/details` | PATCH | test_update_event_detail | ✓ |\n| `/v1/events/search` | GET | test_search_events | ✓ |\n| `/v1/events/{eventId}` | GET | test_read_event | ✓ |\n| `/v1/events/{eventId}/ticketItems` | POST | test_create_free_ticket_item | ✓ |\n| `/v1/events/{eventId}/ticketItems` | GET | test_get_event_ticket_items | ✓ |\n| `/v1/events/{eventId}/open` | PATCH | test_open_event | ✓ |\n| `/v1/carts` | POST | test_create_cart | ✓ |\n| `/v1/carts/recent` | GET | test_read_cart | ✓ |\n| `/v1/orders/` | POST | test_create_order | ✓ |\n| `/v1/orders/{orderUuid}/free` | POST | test_free_order | ✓ |\n| `/v1/orders/{orderUuid}` | GET | test_read_order | ✓ |\n| `/v1/orders/{orderUuid}/tickets` | GET | test_read_order_tickets | ✓ |\n| `/v1/orders` | GET | test_read_my_orders | ✓ |\n| `/v1/events/{eventId}/comments` | POST | test_create_comment | ✓ |\n| `/v1/events/{eventId}/comments` | GET | test_read_comments | ✓ |\n| `/v1/events/{eventId}/comments/counts` | GET | test_get_comment_counts | ✓ |\n| `/v1/orders/{orderUuid}/refund` | POST | test_refund_order | ✓ |\n\n---\n\n## 공유 상태 흐름 (TestState)\n\n### 상태 관리 구조\n\n`conftest.py`의 `TestState` 클래스는 **세션 스코프 픽스처**로 전체 테스트 실행 중 단 하나의 인스턴스만 유지됩니다.\n\n```python\nclass TestState:\n    access_token: str = \"\"           # 로그인 토큰\n    refresh_token: str = \"\"          # 토큰 갱신용\n    host_id: int = 0                 # 호스트 ID\n    event_id: int = 0                # 이벤트 ID\n    ticket_item_id: int = 0          # 티켓 상품 ID\n    cart_id: int = 0                 # 장바구니 ID\n    order_uuid: str = \"\"             # 주문 UUID\n    comment_id: int = 0              # 응원글 ID\n    refund_order_uuid: str = \"\"      # 환불 대상 주문 UUID\n```\n\n### 데이터 전달 흐름\n\n```\ntest_01_auth\n  ↓ state.access_token, state.refresh_token 저장\ntest_02_host\n  ↓ state.host_id 저장\ntest_03_event\n  ↓ state.event_id 저장\ntest_04_ticket_item\n  ↓ state.ticket_item_id 저장 + event 오픈\ntest_05_order_flow (핵심 플로우)\n  ↓ state.cart_id → state.order_uuid 저장\ntest_06_issued_ticket\n  ↓ order_uuid로 발급 티켓 조회\ntest_07_comment\n  ↓ state.comment_id 저장\ntest_08_refund\n  ↓ 새로운 주문 생성 후 환불\n\ntest_09~18: 에러/라이프사이클/멀티유저/재고/CRUD/상태 매트릭스\n\ntest_19_host_invite\n  ↓ 호스트 초대/가입/거절/역할/Slack URL\ntest_20_ticket_options\n  ↓ 옵션 그룹 CRUD, 티켓 적용/해제, 삭제\ntest_21_dudoong_ticket\n  ↓ 두둥티켓 승인/거절 플로우\ntest_22_order_detail_verification\n  ↓ 주문 응답 필드 상세 검증\ntest_23_refund_edge_cases\n  ↓ 이중 환불, 재고 복원, 티켓 취소 상태\ntest_24_admin_features\n  ↓ 관리자 주문/티켓 테이블, 입장 처리, 이중 입장\ntest_25_coupon_flow\n  ↓ 쿠폰 캠페인/발급/적용\ntest_26_order_edge_cases\n  ↓ 재고 소진, 타유저 조회 차단, 중복 결제, 미인증\n```\n\n### 의존성 검증\n\n각 테스트는 필요한 리소스의 존재 여부를 확인합니다:\n\n```python\ndef test_create_event(base_url, auth_headers, state):\n    assert state.host_id, \"host_id가 없습니다. test_02_host를 먼저 실행하세요.\"\n    # host_id가 없으면 AssertionError로 테스트 실패\n```\n\n---\n\n## 외부 API 처리\n\n### 로컬 개발 환경의 API 모킹\n\n실제 외부 API를 호출하지 않고 로컬에서 즉시 응답하기 위해 다음과 같이 처리됩니다:\n\n#### 1) Kakao OAuth 로그인\n- **엔드포인트**: `POST /v1/auth/oauth/local/login`\n- **방식**: Spring Security 설정에서 `LocalAuthController` 제공\n- **역할**: 실제 Kakao와 통신하지 않고 테스트 사용자 정보로 즉시 로그인\n- **요청**:\n  ```json\n  {\n    \"email\": \"test@dudoong.com\",\n    \"name\": \"E2E테스터\",\n    \"phoneNumber\": \"010-0000-0000\",\n    \"profileImage\": null,\n    \"marketingAgree\": false\n  }\n  ```\n- **응답**:\n  ```json\n  {\n    \"status\": 200,\n    \"data\": {\n      \"accessToken\": \"eyJ0eXAiOiJKV1QiLC...\",\n      \"refreshToken\": \"eyJ0eXAiOiJKV1QiLC...\"\n    }\n  }\n  ```\n\n#### 2) Toss Payments (결제)\n- **호출 지점**: 유료 결제 시 (현재 테스트에는 무료 결제만 사용)\n- **로컬 처리**: 무료 주문은 `/v1/orders/{orderUuid}/free` 엔드포인트로 처리\n- **외부 API 호출 안 함**: E2E 테스트는 무료 티켓만 생성하므로 Toss API 호출 없음\n\n#### 3) AWS S3 (이미지 업로드)\n- **호출 지점**: 이벤트 포스터 업로드 시\n- **현재 테스트**: 이미지 키만 저장 (실제 파일 업로드 안 함)\n- **향후**: `ImageController` 프리사인드 URL 엔드포인트로 확장 가능\n\n#### 4) Slack 알림\n- **호출 지점**: 정산 완료, 예매 안내 등\n- **로컬 처리**: Slack 설정이 활성화되지 않거나 Mock으로 처리\n- **E2E 테스트**: Slack 호출 없음 (선택사항)\n\n#### 5) NCP AlimTalk (문자 알림)\n- **호출 지점**: 예매 확정, 입장 안내 등\n- **로컬 처리**: NCP API 설정이 비활성화됨\n- **E2E 테스트**: AlimTalk 호출 없음\n\n### 로컬 테스트 실행 시 주의사항\n\n- **데이터베이스**: 로컬 MySQL 필수 (테스트마다 새로운 데이터 생성)\n- **Redis**: Optional (캐시/락 기능이 필요한 경우)\n- **외부 API 토큰**: application.properties에서 Mock 설정 확인\n\n---\n\n## 헬퍼 함수 및 유틸리티\n\n### conftest.py 제공 함수\n\n#### `assert_status(response, expected_status)`\nHTTP 응답 상태 코드를 검증합니다.\n```python\nassert_status(resp, 200)  # 200이 아니면 AssertionError + 응답 본문 출력\n```\n\n#### `get_data(response)`\n`SuccessResponseAdvice` 래핑된 응답에서 `data` 필드를 추출합니다.\n```python\nresp = requests.get(url)\ndata = get_data(resp)\n# {\"status\": 200, \"data\": {...}} → {...}\n```\n\n### 응답 형식\n\n모든 API 응답은 `SuccessResponseAdvice`에 의해 다음과 같이 래핑됩니다:\n\n```json\n{\n  \"status\": 200,\n  \"data\": {\n    \"hostId\": 1,\n    \"name\": \"E2E테스트호스트\",\n    ...\n  }\n}\n```\n\n테스트에서는 항상 `get_data(response)`로 `data` 필드를 추출합니다.\n\n---\n\n## 추가 예정 (미커버 시나리오)\n\n#### 1) 유료 주문 (Toss Payments)\n- [ ] 유료 티켓 상품 생성 + Toss 결제 승인 콜백\n\n#### 2) 호스트 정산\n- [ ] 정산 레포트 조회 / 기간별 집계\n\n#### 3) 이벤트 카테고리\n- [ ] 카테고리별 / 다중 카테고리 검색\n\n---\n\n## 테스트 작성 가이드\n\n### 새로운 테스트 추가 시 체크리스트\n\n1. **파일명**: `test_NN_<domain>.py` 형식 (NN: 실행 순서)\n2. **상태 의존성**: `state`에서 필요한 리소스 확인\n3. **Fixture 사용**: `base_url`, `auth_headers`, `state` 활용\n4. **응답 검증**: `assert_status()` + `get_data()` 조합\n5. **로깅**: 테스트 진행 상황을 `print()`로 출력\n6. **상태 저장**: 생성된 리소스는 `state`에 저장 (다른 테스트에서 재사용)\n\n### 예시 테스트\n\n```python\ndef test_create_something(base_url, auth_headers, state):\n    \"\"\"설명: 무엇을 테스트하는가\"\"\"\n    # 의존성 확인\n    assert state.event_id, \"event_id가 필요합니다.\"\n\n    # API 호출\n    url = f\"{base_url}/v1/something\"\n    payload = {...}\n    print(f\"\\n[test_create_something] POST {url}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[test_create_something] status={resp.status_code}\")\n\n    # 응답 검증\n    assert_status(resp, 200)\n    data = get_data(resp)\n    assert \"someId\" in data\n\n    # 상태 저장 (다음 테스트에서 사용)\n    state.some_id = data[\"someId\"]\n    print(f\"[test_create_something] 생성 완료: id={state.some_id}\")\n```\n\n---\n\n## 트러블슈팅\n\n### 문제: \"host_id가 없습니다\"\n**원인**: test_02_host를 실행하지 않았음\n**해결**: 전체 테스트를 순서대로 실행 (`pytest -v`)\n\n### 문제: \"connect refused\" 또는 \"Connection refused\"\n**원인**: 백엔드 서버가 실행 중이지 않음\n**해결**:\n```bash\n# 서버 실행 확인\ncurl http://localhost:8080/api/v1/auth/oauth/local/login\n\n# 서버 재시작\n./gradlew :DuDoong-Api:bootRun\n```\n\n### 문제: \"API_BASE_URL을 찾을 수 없다\"\n**원인**: 커스텀 URL을 설정하지 않음\n**해결**: 환경 변수 설정 또는 conftest.py의 기본값 확인\n```bash\nAPI_BASE_URL=http://localhost:8080/api pytest -v\n```\n\n### 문제: \"accessToken이 유효하지 않음\" (401)\n**원인**: 토큰 만료 또는 로그인 실패\n**해결**: 전체 테스트 재실행 (auth_token 픽스처가 새로 로그인)\n\n---\n\n## 참고 자료\n\n- **API 문서**: SwaggerUI (http://localhost:8080/swagger-ui.html)\n- **백엔드 코드**: `/DuDoong-Backend/`\n- **Domain 구조**: `/DuDoong-Backend/DuDoong-Domain/src/main/kotlin/`\n- **API 컨트롤러**: `/DuDoong-Backend/DuDoong-Api/src/main/kotlin/`\n"
  },
  {
    "path": "e2e-tests/conftest.py",
    "content": "\"\"\"\nDuDoong Backend E2E 테스트 공통 픽스처 및 공유 상태 정의.\n모든 테스트 모듈에서 이 파일의 fixtures를 사용합니다.\n\nAPI 응답은 SuccessResponseAdvice에 의해 {\"status\":200, \"data\":{...}} 형태로 래핑됩니다.\nget_data() 헬퍼를 사용하여 data 필드를 추출하세요.\n\"\"\"\nimport os\nimport pytest\nimport requests\n\n\nclass TestState:\n    \"\"\"테스트 모듈 간 공유되는 상태 (세션 전체에서 유지)\"\"\"\n    access_token: str = \"\"\n    refresh_token: str = \"\"\n    host_id: int = 0\n    event_id: int = 0\n    ticket_item_id: int = 0\n    cart_id: int = 0\n    order_uuid: str = \"\"\n    comment_id: int = 0\n    # 환불 테스트용 별도 주문\n    refund_order_uuid: str = \"\"\n\n\n@pytest.fixture(scope=\"session\")\ndef state():\n    \"\"\"세션 전체에서 공유되는 TestState 인스턴스를 반환합니다.\"\"\"\n    return TestState()\n\n\n@pytest.fixture(scope=\"session\")\ndef base_url():\n    \"\"\"API 베이스 URL. 환경변수 API_BASE_URL로 재정의 가능합니다.\"\"\"\n    return os.environ.get(\"API_BASE_URL\", \"http://localhost:8080/api\")\n\n\n@pytest.fixture(scope=\"session\")\ndef auth_token(base_url, state):\n    \"\"\"\n    로컬 개발용 로그인을 수행하고 accessToken을 반환합니다.\n    세션 스코프이므로 전체 테스트 실행 중 1회만 로그인합니다.\n    \"\"\"\n    url = f\"{base_url}/v1/auth/oauth/local/login\"\n    payload = {\n        \"email\": \"test@dudoong.com\",\n        \"name\": \"E2E테스터\",\n        \"phoneNumber\": \"010-0000-0000\",\n        \"profileImage\": None,\n        \"marketingAgree\": False,\n    }\n    print(f\"\\n[AUTH] POST {url}\")\n    resp = requests.post(url, json=payload)\n    print(f\"[AUTH] status={resp.status_code}, body={resp.text[:300]}\")\n    assert resp.status_code == 200, f\"로그인 실패: {resp.text}\"\n    data = get_data(resp)\n    state.access_token = data[\"accessToken\"]\n    state.refresh_token = data[\"refreshToken\"]\n    return data[\"accessToken\"]\n\n\n@pytest.fixture(scope=\"session\")\ndef auth_headers(auth_token):\n    \"\"\"Authorization Bearer 헤더 딕셔너리를 반환합니다.\"\"\"\n    return {\"Authorization\": f\"Bearer {auth_token}\"}\n\n\ndef assert_status(response, expected_status):\n    \"\"\"응답 상태코드를 검증하는 헬퍼 함수입니다.\"\"\"\n    assert response.status_code == expected_status, (\n        f\"기대 상태코드 {expected_status}, 실제: {response.status_code}\\n\"\n        f\"응답 본문: {response.text[:500]}\"\n    )\n\n\ndef get_data(response):\n    \"\"\"SuccessResponseAdvice 래핑된 응답에서 data 필드를 추출합니다.\"\"\"\n    body = response.json()\n    if \"data\" in body:\n        return body[\"data\"]\n    return body\n"
  },
  {
    "path": "e2e-tests/requirements.txt",
    "content": "pytest==7.4.4\nrequests==2.31.0\n"
  },
  {
    "path": "e2e-tests/test_01_auth.py",
    "content": "\"\"\"\n인증 관련 E2E 시나리오 테스트.\n로컬 개발용 로그인, 토큰 갱신, 헬스체크, 미인증 접근 검증을 포함합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data, get_data\n\n\ndef test_local_login(base_url, state):\n    \"\"\"로컬 개발용 즉시 로그인이 성공하고 accessToken/refreshToken이 반환되는지 확인합니다.\"\"\"\n    url = f\"{base_url}/v1/auth/oauth/local/login\"\n    payload = {\n        \"email\": \"test@dudoong.com\",\n        \"name\": \"E2E테스터\",\n        \"phoneNumber\": \"010-0000-0000\",\n        \"profileImage\": None,\n        \"marketingAgree\": False,\n    }\n    print(f\"\\n[test_local_login] POST {url}\")\n    resp = requests.post(url, json=payload)\n    print(f\"[test_local_login] status={resp.status_code}, body={resp.text[:300]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    assert \"accessToken\" in data, \"accessToken 필드가 응답에 없습니다\"\n    assert \"refreshToken\" in data, \"refreshToken 필드가 응답에 없습니다\"\n    assert data[\"accessToken\"], \"accessToken이 비어 있습니다\"\n    assert data[\"refreshToken\"], \"refreshToken이 비어 있습니다\"\n\n    # state에 저장 (다른 테스트에서 재사용)\n    state.access_token = data[\"accessToken\"]\n    state.refresh_token = data[\"refreshToken\"]\n    print(f\"[test_local_login] accessToken 획득 완료: {data['accessToken'][:30]}...\")\n\n\ndef test_token_refresh(base_url, state):\n    \"\"\"refreshToken으로 토큰을 갱신하면 새로운 토큰이 반환되는지 확인합니다.\"\"\"\n    assert state.refresh_token, \"refresh_token이 없습니다. test_local_login을 먼저 실행하세요.\"\n    url = f\"{base_url}/v1/auth/token/refresh\"\n    params = {\"token\": state.refresh_token}\n    print(f\"\\n[test_token_refresh] POST {url} ?token=***\")\n    resp = requests.post(url, params=params)\n    print(f\"[test_token_refresh] status={resp.status_code}, body={resp.text[:300]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    assert \"accessToken\" in data, \"accessToken 필드가 응답에 없습니다\"\n    assert \"refreshToken\" in data, \"refreshToken 필드가 응답에 없습니다\"\n    # 갱신된 토큰으로 state 업데이트\n    state.access_token = data[\"accessToken\"]\n    state.refresh_token = data[\"refreshToken\"]\n    print(f\"[test_token_refresh] 토큰 갱신 완료\")\n\n\ndef test_unauthenticated_access(base_url):\n    \"\"\"인증 토큰 없이 보호된 엔드포인트에 접근하면 401 또는 403이 반환되는지 확인합니다.\"\"\"\n    url = f\"{base_url}/v1/hosts\"\n    print(f\"\\n[test_unauthenticated_access] GET {url} (토큰 없음)\")\n    resp = requests.get(url)\n    print(f\"[test_unauthenticated_access] status={resp.status_code}\")\n\n    assert resp.status_code in (401, 403), (\n        f\"미인증 접근 시 401 또는 403이 기대되지만 {resp.status_code}가 반환되었습니다\"\n    )\n    print(f\"[test_unauthenticated_access] 미인증 접근 차단 확인: {resp.status_code}\")\n"
  },
  {
    "path": "e2e-tests/test_02_host.py",
    "content": "\"\"\"\n호스트 관련 E2E 시나리오 테스트.\n호스트 생성 및 내가 속한 호스트 목록 조회를 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data, get_data\n\n\ndef test_create_host(base_url, auth_headers, state):\n    \"\"\"새로운 호스트를 생성하고 host_id를 state에 저장합니다.\"\"\"\n    url = f\"{base_url}/v1/hosts\"\n    payload = {\n        \"name\": \"E2E테스트호스트\",\n        \"contactEmail\": \"host@dudoong.com\",\n        \"contactNumber\": \"010-1234-5678\",\n    }\n    print(f\"\\n[test_create_host] POST {url}\")\n    print(f\"[test_create_host] payload={payload}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[test_create_host] status={resp.status_code}, body={resp.text[:400]}\")\n\n    # 201 또는 200 모두 성공으로 허용\n    assert resp.status_code in (200, 201), (\n        f\"호스트 생성 실패: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    data = get_data(resp)\n    assert \"hostId\" in data, f\"응답에 hostId 필드가 없습니다: {data}\"\n    state.host_id = data[\"hostId\"]\n    print(f\"[test_create_host] 호스트 생성 완료: host_id={state.host_id}\")\n\n\ndef test_read_host_profiles(base_url, auth_headers, state):\n    \"\"\"내가 속한 호스트 목록을 조회하고 생성된 호스트가 포함되는지 확인합니다.\"\"\"\n    url = f\"{base_url}/v1/hosts\"\n    print(f\"\\n[test_read_host_profiles] GET {url}\")\n    resp = requests.get(url, headers=auth_headers)\n    print(f\"[test_read_host_profiles] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    # SliceResponse 구조: {\"content\": [...], \"hasNext\": bool}\n    assert \"content\" in data, f\"응답에 content 필드가 없습니다: {data}\"\n    print(f\"[test_read_host_profiles] 호스트 목록 조회 완료: {len(data['content'])}개\")\n    if state.host_id:\n        ids = [h.get(\"hostId\") for h in data[\"content\"]]\n        assert state.host_id in ids, (\n            f\"생성된 host_id={state.host_id}가 목록에 없습니다: {ids}\"\n        )\n        print(f\"[test_read_host_profiles] 생성된 호스트 확인됨: id={state.host_id}\")\n"
  },
  {
    "path": "e2e-tests/test_03_event.py",
    "content": "\"\"\"\n이벤트(공연) 관련 E2E 시나리오 테스트.\n이벤트 생성, 기본 정보 수정, 상세 정보 수정, 상세 조회, 검색을 검증합니다.\n\"\"\"\nimport pytest\nimport requests\nfrom datetime import datetime, timedelta\n\nfrom conftest import assert_status, get_data, get_data\n\n\ndef _future_date_str(days_ahead: int = 30) -> str:\n    \"\"\"현재로부터 days_ahead일 후의 날짜를 'yyyy.MM.dd HH:mm' 형식으로 반환합니다.\"\"\"\n    future = datetime.now() + timedelta(days=days_ahead)\n    return future.strftime(\"%Y.%m.%d %H:%M\")\n\n\ndef test_create_event(base_url, auth_headers, state):\n    \"\"\"새로운 이벤트(공연)를 생성하고 event_id를 state에 저장합니다.\"\"\"\n    assert state.host_id, \"host_id가 없습니다. test_02_host를 먼저 실행하세요.\"\n    url = f\"{base_url}/v1/events\"\n    payload = {\n        \"hostId\": state.host_id,\n        \"name\": \"E2E테스트공연\",\n        \"startAt\": _future_date_str(30),\n        \"runTime\": 90,\n    }\n    print(f\"\\n[test_create_event] POST {url}\")\n    print(f\"[test_create_event] payload={payload}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[test_create_event] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (200, 201), (\n        f\"이벤트 생성 실패: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    data = get_data(resp)\n    assert \"eventId\" in data, f\"응답에 eventId 필드가 없습니다: {data}\"\n    state.event_id = data[\"eventId\"]\n    print(f\"[test_create_event] 이벤트 생성 완료: event_id={state.event_id}\")\n\n\ndef test_update_event_basic(base_url, auth_headers, state):\n    \"\"\"이벤트 기본 정보(이름, 시작시각, 장소 등)를 수정합니다.\"\"\"\n    assert state.event_id, \"event_id가 없습니다. test_create_event를 먼저 실행하세요.\"\n    url = f\"{base_url}/v1/events/{state.event_id}/basic\"\n    payload = {\n        \"name\": \"E2E테스트공연(수정됨)\",\n        \"startAt\": _future_date_str(45),\n        \"runTime\": 120,\n        \"placeName\": \"테스트공연장\",\n        \"placeAddress\": \"서울 마포구 어울마당로 35\",\n        \"longitude\": 126.920036,\n        \"latitude\": 37.548369,\n    }\n    print(f\"\\n[test_update_event_basic] PATCH {url}\")\n    print(f\"[test_update_event_basic] payload={payload}\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_update_event_basic] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    print(f\"[test_update_event_basic] 기본 정보 수정 완료\")\n\n\ndef test_update_event_detail(base_url, auth_headers, state):\n    \"\"\"이벤트 상세 정보(포스터 이미지, 공연 내용)를 수정합니다.\"\"\"\n    assert state.event_id, \"event_id가 없습니다.\"\n    url = f\"{base_url}/v1/events/{state.event_id}/details\"\n    payload = {\n        \"posterImageKey\": \"test/event/e2e/poster.jpeg\",\n        \"content\": \"E2E 테스트 공연에 오신 것을 환영합니다.\",\n    }\n    print(f\"\\n[test_update_event_detail] PATCH {url}\")\n    print(f\"[test_update_event_detail] payload={payload}\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_update_event_detail] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    print(f\"[test_update_event_detail] 상세 정보 수정 완료\")\n\n\ndef test_search_events(base_url):\n    \"\"\"이벤트 이름 키워드 검색이 동작하는지 확인합니다 (인증 불필요).\"\"\"\n    url = f\"{base_url}/v1/events/search\"\n    params = {\"keyword\": \"E2E\"}\n    print(f\"\\n[test_search_events] GET {url} params={params}\")\n    resp = requests.get(url, params=params)\n    print(f\"[test_search_events] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    assert \"content\" in data, f\"응답에 content 필드가 없습니다: {data}\"\n    print(f\"[test_search_events] 검색 결과: {len(data['content'])}개\")\n"
  },
  {
    "path": "e2e-tests/test_04_ticket_item.py",
    "content": "\"\"\"\n티켓 상품 관련 E2E 시나리오 테스트.\n무료 티켓 생성 및 이벤트의 티켓 목록 조회를 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data, get_data\n\n\ndef test_create_free_ticket_item(base_url, auth_headers, state):\n    \"\"\"\n    이벤트에 무료 티켓 상품을 생성하고 ticket_item_id를 state에 저장합니다.\n    payType=무료티켓, approveType=선착순으로 생성합니다.\n    \"\"\"\n    assert state.event_id, \"event_id가 없습니다. test_03_event를 먼저 실행하세요.\"\n    url = f\"{base_url}/v1/events/{state.event_id}/ticketItems\"\n    payload = {\n        \"payType\": \"무료티켓\",\n        \"name\": \"E2E무료티켓\",\n        \"description\": \"E2E 테스트용 무료 입장 티켓입니다.\",\n        \"price\": 0,\n        \"supplyCount\": 100,\n        \"approveType\": \"선착순\",\n        \"isQuantityPublic\": True,\n        \"purchaseLimit\": 2,\n    }\n    print(f\"\\n[test_create_free_ticket_item] POST {url}\")\n    print(f\"[test_create_free_ticket_item] payload={payload}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[test_create_free_ticket_item] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (200, 201), (\n        f\"티켓 상품 생성 실패: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    data = get_data(resp)\n    assert \"ticketItemId\" in data, f\"응답에 ticketItemId 필드가 없습니다: {data}\"\n    state.ticket_item_id = data[\"ticketItemId\"]\n    print(f\"[test_create_free_ticket_item] 티켓 상품 생성 완료: ticket_item_id={state.ticket_item_id}\")\n\n\ndef test_get_event_ticket_items(base_url, state):\n    \"\"\"\n    인증 없이 이벤트의 티켓 상품 목록을 조회합니다.\n    생성된 티켓 상품이 목록에 포함되는지 확인합니다.\n    \"\"\"\n    assert state.event_id, \"event_id가 없습니다.\"\n    url = f\"{base_url}/v1/events/{state.event_id}/ticketItems\"\n    print(f\"\\n[test_get_event_ticket_items] GET {url} (인증 없음)\")\n    resp = requests.get(url)\n    print(f\"[test_get_event_ticket_items] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    # GetEventTicketItemsResponse 구조: {\"ticketItems\": [...]}\n    assert \"ticketItems\" in data, f\"응답에 ticketItems 필드가 없습니다: {data}\"\n    items = data[\"ticketItems\"]\n    print(f\"[test_get_event_ticket_items] 티켓 상품 목록 조회 완료: {len(items)}개\")\n    if state.ticket_item_id:\n        ids = [t.get(\"ticketItemId\") for t in items]\n        assert state.ticket_item_id in ids, (\n            f\"생성된 ticket_item_id={state.ticket_item_id}가 목록에 없습니다: {ids}\"\n        )\n        print(f\"[test_get_event_ticket_items] 생성된 티켓 상품 확인됨: id={state.ticket_item_id}\")\n\n\ndef test_open_event(base_url, auth_headers, state):\n    \"\"\"\n    이벤트를 오픈 상태로 전환합니다.\n    기본 정보, 상세 정보, 티켓 상품이 모두 존재해야 오픈 가능합니다.\n    이후 장바구니/주문 테스트에 필요합니다.\n    \"\"\"\n    assert state.event_id, \"event_id가 없습니다.\"\n    url = f\"{base_url}/v1/events/{state.event_id}/open\"\n    print(f\"\\n[test_open_event] PATCH {url}\")\n    resp = requests.patch(url, headers=auth_headers)\n    print(f\"[test_open_event] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    print(f\"[test_open_event] 이벤트 오픈 완료\")\n\n\ndef test_read_event(base_url, state):\n    \"\"\"인증 없이 오픈된 이벤트 상세 정보를 조회합니다.\"\"\"\n    assert state.event_id, \"event_id가 없습니다.\"\n    url = f\"{base_url}/v1/events/{state.event_id}\"\n    print(f\"\\n[test_read_event] GET {url} (인증 없음)\")\n    resp = requests.get(url)\n    print(f\"[test_read_event] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    assert \"name\" in data, f\"응답에 name 필드가 없습니다: {data}\"\n    print(f\"[test_read_event] 이벤트 상세 조회 완료: name={data.get('name')}\")\n"
  },
  {
    "path": "e2e-tests/test_05_order_flow.py",
    "content": "\"\"\"\n주문 플로우 E2E 시나리오 테스트 (핵심 시나리오).\n장바구니 생성 → 장바구니 조회 → 주문 생성 → 무료 주문 완료 → 주문 상세 조회 순서로 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data, get_data\n\n\ndef test_create_cart(base_url, auth_headers, state):\n    \"\"\"\n    티켓 상품을 장바구니에 담고 cart_id를 state에 저장합니다.\n    옵션이 없는 무료 티켓이므로 options는 빈 리스트로 전송합니다.\n    \"\"\"\n    assert state.ticket_item_id, \"ticket_item_id가 없습니다. test_04_ticket_item을 먼저 실행하세요.\"\n    url = f\"{base_url}/v1/carts\"\n    payload = {\n        \"items\": [\n            {\n                \"itemId\": state.ticket_item_id,\n                \"quantity\": 1,\n                \"options\": [],\n            }\n        ]\n    }\n    print(f\"\\n[test_create_cart] POST {url}\")\n    print(f\"[test_create_cart] payload={payload}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[test_create_cart] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (200, 201), (\n        f\"장바구니 생성 실패: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    data = get_data(resp)\n    assert \"cartId\" in data, f\"응답에 cartId 필드가 없습니다: {data}\"\n    state.cart_id = data[\"cartId\"]\n    print(f\"[test_create_cart] 장바구니 생성 완료: cart_id={state.cart_id}\")\n\n\ndef test_read_cart(base_url, auth_headers, state):\n    \"\"\"최근 생성된 장바구니를 조회합니다.\"\"\"\n    url = f\"{base_url}/v1/carts/recent\"\n    print(f\"\\n[test_read_cart] GET {url}\")\n    resp = requests.get(url, headers=auth_headers)\n    print(f\"[test_read_cart] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    # 장바구니가 없을 경우 null 반환 가능\n    if data is not None:\n        assert \"cartId\" in data, f\"응답에 cartId 필드가 없습니다: {data}\"\n        print(f\"[test_read_cart] 장바구니 조회 완료: cart_id={data.get('cartId')}\")\n    else:\n        print(f\"[test_read_cart] 장바구니 없음 (null 반환)\")\n\n\ndef test_create_order(base_url, auth_headers, state):\n    \"\"\"\n    장바구니를 주문서로 변환하여 주문을 생성하고 order_uuid를 state에 저장합니다.\n    쿠폰 없이 생성합니다 (couponId=null).\n    \"\"\"\n    assert state.cart_id, \"cart_id가 없습니다. test_create_cart를 먼저 실행하세요.\"\n    url = f\"{base_url}/v1/orders/\"\n    payload = {\n        \"couponId\": None,\n        \"cartId\": state.cart_id,\n    }\n    print(f\"\\n[test_create_order] POST {url}\")\n    print(f\"[test_create_order] payload={payload}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[test_create_order] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (200, 201), (\n        f\"주문 생성 실패: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    data = get_data(resp)\n    assert \"orderId\" in data, f\"응답에 orderId 필드가 없습니다: {data}\"\n    state.order_uuid = data[\"orderId\"]\n    print(f\"[test_create_order] 주문 생성 완료: order_uuid={state.order_uuid}\")\n\n\ndef test_free_order(base_url, auth_headers, state):\n    \"\"\"\n    0원 무료 주문을 완료 처리합니다.\n    선착순 방식 무료 티켓에만 적용 가능합니다.\n    \"\"\"\n    assert state.order_uuid, \"order_uuid가 없습니다. test_create_order를 먼저 실행하세요.\"\n    url = f\"{base_url}/v1/orders/{state.order_uuid}/free\"\n    print(f\"\\n[test_free_order] POST {url}\")\n    resp = requests.post(url, headers=auth_headers)\n    print(f\"[test_free_order] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    assert \"orderUuid\" in data, f\"응답에 orderUuid 필드가 없습니다: {data}\"\n    print(f\"[test_free_order] 무료 주문 완료: orderUuid={data.get('orderUuid')}\")\n\n\ndef test_read_order(base_url, auth_headers, state):\n    \"\"\"완료된 주문의 상세 정보를 조회하고 발급된 티켓이 포함되는지 확인합니다.\"\"\"\n    assert state.order_uuid, \"order_uuid가 없습니다.\"\n    url = f\"{base_url}/v1/orders/{state.order_uuid}\"\n    print(f\"\\n[test_read_order] GET {url}\")\n    resp = requests.get(url, headers=auth_headers)\n    print(f\"[test_read_order] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    assert \"orderUuid\" in data, f\"응답에 orderUuid 필드가 없습니다: {data}\"\n    assert data[\"orderUuid\"] == state.order_uuid, \"반환된 orderUuid가 일치하지 않습니다\"\n    print(f\"[test_read_order] 주문 상세 조회 완료: {data.get('orderUuid')}\")\n"
  },
  {
    "path": "e2e-tests/test_06_issued_ticket.py",
    "content": "\"\"\"\n발급 티켓 관련 E2E 시나리오 테스트.\n주문 완료 후 발급된 티켓 목록 및 상세 조회를 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data, get_data\n\n\ndef test_read_order_tickets(base_url, auth_headers, state):\n    \"\"\"\n    완료된 주문에서 발급된 티켓 목록을 조회합니다.\n    주문 완료 후 티켓이 정상적으로 발급되었는지 확인합니다.\n    \"\"\"\n    assert state.order_uuid, \"order_uuid가 없습니다. test_05_order_flow를 먼저 실행하세요.\"\n    url = f\"{base_url}/v1/orders/{state.order_uuid}/tickets\"\n    print(f\"\\n[test_read_order_tickets] GET {url}\")\n    resp = requests.get(url, headers=auth_headers)\n    print(f\"[test_read_order_tickets] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    # OrderTicketResponse 구조 확인\n    print(f\"[test_read_order_tickets] 주문 티켓 조회 완료: {data}\")\n\n\ndef test_read_my_orders(base_url, auth_headers, state):\n    \"\"\"\n    마이페이지 예매 목록에서 완료된 주문이 조회되는지 확인합니다.\n    showing=true 파라미터로 현재 유효한 예매 목록을 조회합니다.\n    \"\"\"\n    url = f\"{base_url}/v1/orders\"\n    params = {\"showing\": \"true\"}\n    print(f\"\\n[test_read_my_orders] GET {url} params={params}\")\n    resp = requests.get(url, params=params, headers=auth_headers)\n    print(f\"[test_read_my_orders] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    assert \"content\" in data, f\"응답에 content 필드가 없습니다: {data}\"\n    print(f\"[test_read_my_orders] 예매 목록 조회 완료: {len(data['content'])}개\")\n    if state.order_uuid:\n        uuids = [o.get(\"orderId\") for o in data[\"content\"]]\n        print(f\"[test_read_my_orders] 조회된 order UUIDs: {uuids}\")\n"
  },
  {
    "path": "e2e-tests/test_07_comment.py",
    "content": "\"\"\"\n응원톡(댓글) 관련 E2E 시나리오 테스트.\n응원글 생성 및 목록 조회를 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data, get_data\n\n\ndef test_create_comment(base_url, auth_headers, state):\n    \"\"\"\n    이벤트에 응원글을 생성하고 comment_id를 state에 저장합니다.\n    nickName은 1~10자, content는 1~150자 제한이 있습니다.\n    \"\"\"\n    assert state.event_id, \"event_id가 없습니다. test_03_event를 먼저 실행하세요.\"\n    url = f\"{base_url}/v1/events/{state.event_id}/comments\"\n    payload = {\n        \"nickName\": \"E2E응원단\",\n        \"content\": \"E2E 테스트 응원합니다! 화이팅!\",\n    }\n    print(f\"\\n[test_create_comment] POST {url}\")\n    print(f\"[test_create_comment] payload={payload}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[test_create_comment] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (200, 201), (\n        f\"응원글 생성 실패: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    data = get_data(resp)\n    assert \"id\" in data, f\"응답에 id 필드가 없습니다: {data}\"\n    state.comment_id = data[\"id\"]\n    print(f\"[test_create_comment] 응원글 생성 완료: comment_id={state.comment_id}\")\n\n\ndef test_read_comments(base_url, state):\n    \"\"\"\n    인증 없이 이벤트의 응원글 목록을 조회합니다.\n    생성된 응원글이 목록에 포함되는지 확인합니다.\n    \"\"\"\n    assert state.event_id, \"event_id가 없습니다.\"\n    url = f\"{base_url}/v1/events/{state.event_id}/comments\"\n    print(f\"\\n[test_read_comments] GET {url} (인증 없음)\")\n    resp = requests.get(url)\n    print(f\"[test_read_comments] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    assert \"content\" in data, f\"응답에 content 필드가 없습니다: {data}\"\n    print(f\"[test_read_comments] 응원글 목록 조회 완료: {len(data['content'])}개\")\n\n\ndef test_get_comment_counts(base_url, state):\n    \"\"\"인증 없이 이벤트의 응원글 개수를 조회합니다.\"\"\"\n    assert state.event_id, \"event_id가 없습니다.\"\n    url = f\"{base_url}/v1/events/{state.event_id}/comments/counts\"\n    print(f\"\\n[test_get_comment_counts] GET {url} (인증 없음)\")\n    resp = requests.get(url)\n    print(f\"[test_get_comment_counts] status={resp.status_code}, body={resp.text[:200]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    assert \"commentCounts\" in data, f\"응답에 commentCounts 필드가 없습니다: {data}\"\n    print(f\"[test_get_comment_counts] 응원글 개수: {data.get('commentCounts')}\")\n"
  },
  {
    "path": "e2e-tests/test_08_refund.py",
    "content": "\"\"\"\n환불 E2E 시나리오 테스트.\n새로운 주문을 생성한 후 환불 처리까지의 전체 플로우를 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data, get_data\n\n\ndef _create_new_order_for_refund(base_url, auth_headers, state) -> str:\n    \"\"\"\n    환불 테스트 전용 새 주문을 생성합니다.\n    cart 생성 → order 생성 → free 결제 순서로 진행하고 order_uuid를 반환합니다.\n    \"\"\"\n    assert state.ticket_item_id, \"ticket_item_id가 없습니다. test_04를 먼저 실행하세요.\"\n\n    # 1) 장바구니 생성\n    cart_resp = requests.post(\n        f\"{base_url}/v1/carts\",\n        json={\n            \"items\": [\n                {\n                    \"itemId\": state.ticket_item_id,\n                    \"quantity\": 1,\n                    \"options\": [],\n                }\n            ]\n        },\n        headers=auth_headers,\n    )\n    print(f\"[refund setup] cart status={cart_resp.status_code}, body={cart_resp.text[:300]}\")\n    assert cart_resp.status_code in (200, 201), f\"환불용 장바구니 생성 실패: {cart_resp.text[:200]}\"\n    cart_id = get_data(cart_resp)[\"cartId\"]\n\n    # 2) 주문 생성\n    order_resp = requests.post(\n        f\"{base_url}/v1/orders/\",\n        json={\"couponId\": None, \"cartId\": cart_id},\n        headers=auth_headers,\n    )\n    print(f\"[refund setup] order status={order_resp.status_code}, body={order_resp.text[:300]}\")\n    assert order_resp.status_code in (200, 201), f\"환불용 주문 생성 실패: {order_resp.text[:200]}\"\n    order_uuid = get_data(order_resp)[\"orderId\"]\n\n    # 3) 무료 결제 완료\n    free_resp = requests.post(\n        f\"{base_url}/v1/orders/{order_uuid}/free\",\n        headers=auth_headers,\n    )\n    print(f\"[refund setup] free status={free_resp.status_code}, body={free_resp.text[:300]}\")\n    assert free_resp.status_code == 200, f\"환불용 무료 결제 실패: {free_resp.text[:200]}\"\n\n    return order_uuid\n\n\ndef test_refund_order(base_url, auth_headers, state):\n    \"\"\"\n    새로운 주문을 생성하고 완료 처리한 뒤 환불을 요청합니다.\n    환불 후 응답에 orderId가 포함되는지 확인합니다.\n    \"\"\"\n    print(f\"\\n[test_refund_order] 환불 테스트용 주문 생성 중...\")\n    refund_order_uuid = _create_new_order_for_refund(base_url, auth_headers, state)\n    state.refund_order_uuid = refund_order_uuid\n    print(f\"[test_refund_order] 환불 대상 주문: {refund_order_uuid}\")\n\n    url = f\"{base_url}/v1/orders/{refund_order_uuid}/refund\"\n    print(f\"[test_refund_order] POST {url}\")\n    resp = requests.post(url, headers=auth_headers)\n    print(f\"[test_refund_order] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    assert \"orderUuid\" in data, f\"응답에 orderUuid 필드가 없습니다: {data}\"\n    assert data[\"orderUuid\"] == refund_order_uuid, \"환불된 orderUuid가 일치하지 않습니다\"\n    print(f\"[test_refund_order] 환불 완료: orderUuid={data.get('orderUuid')}\")\n"
  },
  {
    "path": "e2e-tests/test_09_error_cases.py",
    "content": "\"\"\"\n에러/예외 케이스 E2E 시나리오 테스트.\n유효성 검증 오류, 존재하지 않는 리소스, 인증 오류, 비즈니스 규칙 위반을 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\ndef test_create_host_empty_name(base_url, auth_headers):\n    \"\"\"빈 이름으로 호스트를 생성하면 400 Bad Request가 반환되는지 확인합니다.\"\"\"\n    url = f\"{base_url}/v1/hosts\"\n    payload = {\n        \"name\": \"\",\n        \"contactEmail\": \"host@dudoong.com\",\n        \"contactNumber\": \"010-1234-5678\",\n    }\n    print(f\"\\n[test_create_host_empty_name] POST {url}\")\n    print(f\"[test_create_host_empty_name] payload={payload}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[test_create_host_empty_name] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code == 400, (\n        f\"빈 이름 호스트 생성 시 400이 기대되지만 {resp.status_code}가 반환되었습니다\"\n    )\n    print(f\"[test_create_host_empty_name] 빈 이름 유효성 검증 확인: {resp.status_code}\")\n\n\ndef test_create_event_missing_fields(base_url, auth_headers):\n    \"\"\"hostId 없이 이벤트를 생성하면 400 Bad Request가 반환되는지 확인합니다.\"\"\"\n    url = f\"{base_url}/v1/events\"\n    payload = {\n        \"name\": \"에러테스트공연\",\n        # hostId 누락\n    }\n    print(f\"\\n[test_create_event_missing_fields] POST {url}\")\n    print(f\"[test_create_event_missing_fields] payload={payload}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[test_create_event_missing_fields] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code == 400, (\n        f\"필수 필드 누락 시 400이 기대되지만 {resp.status_code}가 반환되었습니다\"\n    )\n    print(f\"[test_create_event_missing_fields] 필수 필드 누락 유효성 검증 확인: {resp.status_code}\")\n\n\ndef test_create_ticket_invalid_price(base_url, auth_headers, state):\n    \"\"\"음수 가격으로 티켓을 생성하면 400 Bad Request가 반환되는지 확인합니다.\"\"\"\n    if not state.event_id:\n        pytest.skip(\"event_id가 없어 테스트를 건너뜁니다.\")\n    url = f\"{base_url}/v1/events/{state.event_id}/ticketItems\"\n    payload = {\n        \"payType\": \"유료티켓\",\n        \"name\": \"음수가격티켓\",\n        \"description\": \"잘못된 가격 테스트\",\n        \"price\": -1000,\n        \"supplyCount\": 10,\n        \"approveType\": \"선착순\",\n        \"isQuantityPublic\": True,\n        \"purchaseLimit\": 1,\n    }\n    print(f\"\\n[test_create_ticket_invalid_price] POST {url}\")\n    print(f\"[test_create_ticket_invalid_price] payload={payload}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[test_create_ticket_invalid_price] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code == 400, (\n        f\"음수 가격 티켓 생성 시 400이 기대되지만 {resp.status_code}가 반환되었습니다\"\n    )\n    print(f\"[test_create_ticket_invalid_price] 음수 가격 유효성 검증 확인: {resp.status_code}\")\n\n\ndef test_read_nonexistent_event(base_url):\n    \"\"\"존재하지 않는 이벤트 ID로 조회하면 404 Not Found가 반환되는지 확인합니다.\"\"\"\n    url = f\"{base_url}/v1/events/99999999\"\n    print(f\"\\n[test_read_nonexistent_event] GET {url}\")\n    resp = requests.get(url)\n    print(f\"[test_read_nonexistent_event] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code == 404, (\n        f\"존재하지 않는 이벤트 조회 시 404가 기대되지만 {resp.status_code}가 반환되었습니다\"\n    )\n    print(f\"[test_read_nonexistent_event] 404 Not Found 확인\")\n\n\ndef test_expired_token(base_url):\n    \"\"\"쓰레기 토큰으로 인증이 필요한 엔드포인트에 접근하면 401 또는 403이 반환되는지 확인합니다.\"\"\"\n    url = f\"{base_url}/v1/hosts\"\n    garbage_headers = {\"Authorization\": \"Bearer this.is.garbage.token\"}\n    print(f\"\\n[test_expired_token] GET {url} (쓰레기 토큰)\")\n    resp = requests.get(url, headers=garbage_headers)\n    print(f\"[test_expired_token] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (401, 403), (\n        f\"유효하지 않은 토큰 사용 시 401 또는 403이 기대되지만 {resp.status_code}가 반환되었습니다\"\n    )\n    print(f\"[test_expired_token] 유효하지 않은 토큰 차단 확인: {resp.status_code}\")\n\n\ndef test_open_event_without_ticket(base_url, auth_headers, state):\n    \"\"\"\n    티켓 상품이 없는 이벤트를 오픈하려 하면 실패해야 합니다.\n    새 이벤트를 생성하고 기본/상세 정보 없이 바로 오픈 시도합니다.\n    \"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    from datetime import datetime, timedelta\n    future = (datetime.now() + timedelta(days=60)).strftime(\"%Y.%m.%d %H:%M\")\n\n    # 새 이벤트 생성 (티켓 없음)\n    create_url = f\"{base_url}/v1/events\"\n    create_payload = {\n        \"hostId\": state.host_id,\n        \"name\": \"티켓없는테스트공연\",\n        \"startAt\": future,\n        \"runTime\": 60,\n    }\n    print(f\"\\n[test_open_event_without_ticket] POST {create_url}\")\n    create_resp = requests.post(create_url, json=create_payload, headers=auth_headers)\n    print(f\"[test_open_event_without_ticket] create status={create_resp.status_code}, body={create_resp.text[:300]}\")\n\n    if create_resp.status_code not in (200, 201):\n        pytest.skip(f\"이벤트 생성 실패로 테스트를 건너뜁니다: {create_resp.text[:200]}\")\n\n    new_event_id = get_data(create_resp)[\"eventId\"]\n    print(f\"[test_open_event_without_ticket] 새 이벤트 생성됨: event_id={new_event_id}\")\n\n    # 티켓 없이 바로 오픈 시도\n    open_url = f\"{base_url}/v1/events/{new_event_id}/open\"\n    print(f\"[test_open_event_without_ticket] PATCH {open_url} (티켓 없음)\")\n    open_resp = requests.patch(open_url, headers=auth_headers)\n    print(f\"[test_open_event_without_ticket] open status={open_resp.status_code}, body={open_resp.text[:400]}\")\n\n    assert open_resp.status_code != 200, (\n        f\"티켓 없는 이벤트 오픈이 성공해서는 안 됩니다. status={open_resp.status_code}\"\n    )\n    print(f\"[test_open_event_without_ticket] 티켓 없는 오픈 차단 확인: {open_resp.status_code}\")\n"
  },
  {
    "path": "e2e-tests/test_10_event_lifecycle.py",
    "content": "\"\"\"\n이벤트 상태 전이 E2E 시나리오 테스트.\nPREPARING → OPEN → CALCULATING → CLOSED 순서의 상태 전이를 검증합니다.\n\"\"\"\nimport pytest\nimport requests\nfrom datetime import datetime, timedelta\n\nfrom conftest import assert_status, get_data\n\n\ndef _future_date_str(days_ahead: int = 30) -> str:\n    \"\"\"현재로부터 days_ahead일 후의 날짜를 'yyyy.MM.dd HH:mm' 형식으로 반환합니다.\"\"\"\n    future = datetime.now() + timedelta(days=days_ahead)\n    return future.strftime(\"%Y.%m.%d %H:%M\")\n\n\n# 이 테스트 모듈 내에서만 사용하는 로컬 상태\n_lifecycle_state: dict = {}\n\n\ndef test_event_status_preparing(base_url, auth_headers, state):\n    \"\"\"이벤트를 새로 생성하면 상태가 PREPARING인지 확인합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events\"\n    payload = {\n        \"hostId\": state.host_id,\n        \"name\": \"라이프사이클테스트공연\",\n        \"startAt\": _future_date_str(30),\n        \"runTime\": 90,\n    }\n    print(f\"\\n[test_event_status_preparing] POST {url}\")\n    print(f\"[test_event_status_preparing] payload={payload}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[test_event_status_preparing] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (200, 201), (\n        f\"이벤트 생성 실패: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    data = get_data(resp)\n    assert \"eventId\" in data, f\"응답에 eventId 필드가 없습니다: {data}\"\n    _lifecycle_state[\"event_id\"] = data[\"eventId\"]\n    print(f\"[test_event_status_preparing] 이벤트 생성 완료: event_id={_lifecycle_state['event_id']}\")\n\n    # 상태 조회\n    detail_url = f\"{base_url}/v1/events/{_lifecycle_state['event_id']}\"\n    detail_resp = requests.get(detail_url, headers=auth_headers)\n    print(f\"[test_event_status_preparing] GET {detail_url} status={detail_resp.status_code}, body={detail_resp.text[:400]}\")\n\n    if detail_resp.status_code == 200:\n        detail_data = get_data(detail_resp)\n        status_val = detail_data.get(\"status\") or detail_data.get(\"eventStatus\")\n        print(f\"[test_event_status_preparing] 이벤트 상태: {status_val}\")\n        if status_val:\n            assert \"PREPARING\" in str(status_val) or \"준비중\" in str(status_val), (\n                f\"생성 직후 상태는 PREPARING이어야 합니다: {status_val}\"\n            )\n    print(f\"[test_event_status_preparing] PREPARING 상태 확인 완료\")\n\n\ndef test_event_status_open(base_url, auth_headers):\n    \"\"\"기본 정보, 상세 정보, 티켓 상품을 추가한 후 이벤트를 오픈하면 상태가 OPEN이 되는지 확인합니다.\"\"\"\n    event_id = _lifecycle_state.get(\"event_id\")\n    if not event_id:\n        pytest.skip(\"lifecycle event_id가 없어 테스트를 건너뜁니다.\")\n\n    # 기본 정보 수정\n    basic_url = f\"{base_url}/v1/events/{event_id}/basic\"\n    basic_payload = {\n        \"name\": \"라이프사이클테스트공연(수정)\",\n        \"startAt\": _future_date_str(45),\n        \"runTime\": 120,\n        \"placeName\": \"테스트공연장\",\n        \"placeAddress\": \"서울 마포구 어울마당로 35\",\n        \"longitude\": 126.920036,\n        \"latitude\": 37.548369,\n    }\n    print(f\"\\n[test_event_status_open] PATCH {basic_url}\")\n    basic_resp = requests.patch(basic_url, json=basic_payload, headers=auth_headers)\n    print(f\"[test_event_status_open] basic status={basic_resp.status_code}, body={basic_resp.text[:300]}\")\n    assert_status(basic_resp, 200)\n\n    # 상세 정보 수정\n    detail_url = f\"{base_url}/v1/events/{event_id}/details\"\n    detail_payload = {\n        \"posterImageKey\": \"test/event/lifecycle/poster.jpeg\",\n        \"content\": \"라이프사이클 테스트 공연입니다.\",\n    }\n    print(f\"[test_event_status_open] PATCH {detail_url}\")\n    detail_resp = requests.patch(detail_url, json=detail_payload, headers=auth_headers)\n    print(f\"[test_event_status_open] detail status={detail_resp.status_code}, body={detail_resp.text[:300]}\")\n    assert_status(detail_resp, 200)\n\n    # 티켓 상품 생성\n    ticket_url = f\"{base_url}/v1/events/{event_id}/ticketItems\"\n    ticket_payload = {\n        \"payType\": \"무료티켓\",\n        \"name\": \"라이프사이클무료티켓\",\n        \"description\": \"라이프사이클 테스트용 티켓\",\n        \"price\": 0,\n        \"supplyCount\": 50,\n        \"approveType\": \"선착순\",\n        \"isQuantityPublic\": True,\n        \"purchaseLimit\": 2,\n    }\n    print(f\"[test_event_status_open] POST {ticket_url}\")\n    ticket_resp = requests.post(ticket_url, json=ticket_payload, headers=auth_headers)\n    print(f\"[test_event_status_open] ticket status={ticket_resp.status_code}, body={ticket_resp.text[:300]}\")\n    assert ticket_resp.status_code in (200, 201), (\n        f\"티켓 생성 실패: {ticket_resp.text[:200]}\"\n    )\n    ticket_data = get_data(ticket_resp)\n    _lifecycle_state[\"ticket_item_id\"] = ticket_data.get(\"ticketItemId\")\n\n    # 이벤트 오픈\n    open_url = f\"{base_url}/v1/events/{event_id}/open\"\n    print(f\"[test_event_status_open] PATCH {open_url}\")\n    open_resp = requests.patch(open_url, headers=auth_headers)\n    print(f\"[test_event_status_open] open status={open_resp.status_code}, body={open_resp.text[:400]}\")\n    assert_status(open_resp, 200)\n\n    # 상태 확인\n    check_resp = requests.get(f\"{base_url}/v1/events/{event_id}\")\n    if check_resp.status_code == 200:\n        check_data = get_data(check_resp)\n        status_val = check_data.get(\"status\") or check_data.get(\"eventStatus\")\n        print(f\"[test_event_status_open] 이벤트 상태: {status_val}\")\n        if status_val:\n            assert any(s in str(status_val) for s in (\"OPEN\", \"오픈\", \"진행중\")), (\n                f\"오픈 후 상태는 OPEN/진행중이어야 합니다: {status_val}\"\n            )\n    print(f\"[test_event_status_open] OPEN 상태 전이 확인 완료\")\n\n\ndef test_event_status_calculating(base_url, auth_headers):\n    \"\"\"이벤트 상태를 정산중(CALCULATING)으로 전환합니다.\"\"\"\n    event_id = _lifecycle_state.get(\"event_id\")\n    if not event_id:\n        pytest.skip(\"lifecycle event_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events/{event_id}/status\"\n    payload = {\"status\": \"CALCULATING\"}\n    print(f\"\\n[test_event_status_calculating] PATCH {url}\")\n    print(f\"[test_event_status_calculating] payload={payload}\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_event_status_calculating] status={resp.status_code}, body={resp.text[:400]}\")\n\n    # 상태 전이 성공 또는 비즈니스 제약으로 실패 모두 허용 (OPEN 상태에서만 가능)\n    print(f\"[test_event_status_calculating] 정산중 전환 응답: {resp.status_code}\")\n    if resp.status_code == 200:\n        check_resp = requests.get(f\"{base_url}/v1/events/{event_id}\", headers=auth_headers)\n        if check_resp.status_code == 200:\n            check_data = get_data(check_resp)\n            status_val = check_data.get(\"status\") or check_data.get(\"eventStatus\")\n            print(f\"[test_event_status_calculating] 이벤트 상태: {status_val}\")\n    print(f\"[test_event_status_calculating] CALCULATING 상태 전이 시도 완료\")\n\n\ndef test_event_status_closed(base_url, auth_headers):\n    \"\"\"이벤트 상태를 정산완료(CLOSED)로 전환합니다.\"\"\"\n    event_id = _lifecycle_state.get(\"event_id\")\n    if not event_id:\n        pytest.skip(\"lifecycle event_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events/{event_id}/status\"\n    payload = {\"status\": \"CLOSED\"}\n    print(f\"\\n[test_event_status_closed] PATCH {url}\")\n    print(f\"[test_event_status_closed] payload={payload}\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_event_status_closed] status={resp.status_code}, body={resp.text[:400]}\")\n\n    print(f\"[test_event_status_closed] 정산완료 전환 응답: {resp.status_code}\")\n    if resp.status_code == 200:\n        check_resp = requests.get(f\"{base_url}/v1/events/{event_id}\", headers=auth_headers)\n        if check_resp.status_code == 200:\n            check_data = get_data(check_resp)\n            status_val = check_data.get(\"status\") or check_data.get(\"eventStatus\")\n            print(f\"[test_event_status_closed] 이벤트 상태: {status_val}\")\n    print(f\"[test_event_status_closed] CLOSED 상태 전이 시도 완료\")\n"
  },
  {
    "path": "e2e-tests/test_11_multi_user.py",
    "content": "\"\"\"\n멀티 유저 시나리오 E2E 테스트.\n두 번째 사용자가 첫 번째 사용자의 이벤트에서 주문하거나,\n첫 번째 사용자의 이벤트를 수정하려 할 때의 권한 검증을 포함합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\n# 이 테스트 모듈 내에서만 사용하는 로컬 상태\n_user2_state: dict = {\n    \"access_token\": \"\",\n    \"auth_headers\": {},\n    \"cart_id\": 0,\n    \"order_uuid\": \"\",\n}\n\n\ndef _login_user2(base_url: str) -> str:\n    \"\"\"두 번째 테스트 유저로 로그인하고 accessToken을 반환합니다.\"\"\"\n    url = f\"{base_url}/v1/auth/oauth/local/login\"\n    payload = {\n        \"email\": \"user2@dudoong.com\",\n        \"name\": \"E2E테스터2\",\n        \"phoneNumber\": \"010-1111-2222\",\n        \"profileImage\": None,\n        \"marketingAgree\": False,\n    }\n    print(f\"[user2 login] POST {url}\")\n    resp = requests.post(url, json=payload)\n    print(f\"[user2 login] status={resp.status_code}, body={resp.text[:300]}\")\n    assert resp.status_code == 200, f\"user2 로그인 실패: {resp.text}\"\n    data = get_data(resp)\n    return data[\"accessToken\"]\n\n\ndef test_create_second_user(base_url):\n    \"\"\"두 번째 사용자(user2@dudoong.com)로 로그인하고 토큰을 획득합니다.\"\"\"\n    print(f\"\\n[test_create_second_user] user2 로그인 시작\")\n    token = _login_user2(base_url)\n    assert token, \"user2 accessToken이 비어 있습니다\"\n    _user2_state[\"access_token\"] = token\n    _user2_state[\"auth_headers\"] = {\"Authorization\": f\"Bearer {token}\"}\n    print(f\"[test_create_second_user] user2 토큰 획득 완료: {token[:30]}...\")\n\n\ndef test_user2_orders_user1_event(base_url, state):\n    \"\"\"user2가 user1의 이벤트에서 장바구니를 만들고 주문을 생성합니다.\"\"\"\n    if not _user2_state[\"access_token\"]:\n        pytest.skip(\"user2 토큰이 없어 테스트를 건너뜁니다.\")\n    if not state.ticket_item_id:\n        pytest.skip(\"ticket_item_id가 없어 테스트를 건너뜁니다.\")\n\n    headers2 = _user2_state[\"auth_headers\"]\n\n    # 장바구니 생성\n    cart_url = f\"{base_url}/v1/carts\"\n    cart_payload = {\n        \"items\": [\n            {\n                \"itemId\": state.ticket_item_id,\n                \"quantity\": 1,\n                \"options\": [],\n            }\n        ]\n    }\n    print(f\"\\n[test_user2_orders_user1_event] POST {cart_url} (user2)\")\n    print(f\"[test_user2_orders_user1_event] payload={cart_payload}\")\n    cart_resp = requests.post(cart_url, json=cart_payload, headers=headers2)\n    print(f\"[test_user2_orders_user1_event] cart status={cart_resp.status_code}, body={cart_resp.text[:400]}\")\n\n    assert cart_resp.status_code in (200, 201), (\n        f\"user2 장바구니 생성 실패: {cart_resp.text[:300]}\"\n    )\n    cart_data = get_data(cart_resp)\n    assert \"cartId\" in cart_data, f\"응답에 cartId가 없습니다: {cart_data}\"\n    _user2_state[\"cart_id\"] = cart_data[\"cartId\"]\n    print(f\"[test_user2_orders_user1_event] user2 장바구니 생성: cart_id={_user2_state['cart_id']}\")\n\n    # 주문 생성\n    order_url = f\"{base_url}/v1/orders/\"\n    order_payload = {\"couponId\": None, \"cartId\": _user2_state[\"cart_id\"]}\n    print(f\"[test_user2_orders_user1_event] POST {order_url} (user2)\")\n    order_resp = requests.post(order_url, json=order_payload, headers=headers2)\n    print(f\"[test_user2_orders_user1_event] order status={order_resp.status_code}, body={order_resp.text[:400]}\")\n\n    assert order_resp.status_code in (200, 201), (\n        f\"user2 주문 생성 실패: {order_resp.text[:300]}\"\n    )\n    order_data = get_data(order_resp)\n    assert \"orderId\" in order_data, f\"응답에 orderId가 없습니다: {order_data}\"\n    _user2_state[\"order_uuid\"] = order_data[\"orderId\"]\n    print(f\"[test_user2_orders_user1_event] user2 주문 생성 완료: order_uuid={_user2_state['order_uuid']}\")\n\n    # 무료 결제 완료\n    free_url = f\"{base_url}/v1/orders/{_user2_state['order_uuid']}/free\"\n    print(f\"[test_user2_orders_user1_event] POST {free_url} (user2)\")\n    free_resp = requests.post(free_url, headers=headers2)\n    print(f\"[test_user2_orders_user1_event] free status={free_resp.status_code}, body={free_resp.text[:400]}\")\n    assert_status(free_resp, 200)\n    print(f\"[test_user2_orders_user1_event] user2 무료 주문 완료\")\n\n\ndef test_user2_cannot_modify_user1_event(base_url, state):\n    \"\"\"user2가 user1의 이벤트 기본 정보를 수정하려 하면 권한 오류가 발생해야 합니다.\"\"\"\n    if not _user2_state[\"access_token\"]:\n        pytest.skip(\"user2 토큰이 없어 테스트를 건너뜁니다.\")\n    if not state.event_id:\n        pytest.skip(\"event_id가 없어 테스트를 건너뜁니다.\")\n\n    headers2 = _user2_state[\"auth_headers\"]\n    url = f\"{base_url}/v1/events/{state.event_id}/basic\"\n    payload = {\n        \"name\": \"해킹시도공연\",\n        \"startAt\": \"2099.01.01 12:00\",\n        \"runTime\": 999,\n    }\n    print(f\"\\n[test_user2_cannot_modify_user1_event] PATCH {url} (user2 - 권한 없음)\")\n    print(f\"[test_user2_cannot_modify_user1_event] payload={payload}\")\n    resp = requests.patch(url, json=payload, headers=headers2)\n    print(f\"[test_user2_cannot_modify_user1_event] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (400, 401, 403, 404), (\n        f\"타 유저 이벤트 수정 시 권한 오류(400/401/403/404)가 기대되지만 {resp.status_code}가 반환되었습니다\"\n    )\n    print(f\"[test_user2_cannot_modify_user1_event] 타 유저 이벤트 수정 차단 확인: {resp.status_code}\")\n"
  },
  {
    "path": "e2e-tests/test_12_host_management.py",
    "content": "\"\"\"\n호스트 관리 E2E 시나리오 테스트.\n호스트 프로필 수정 및 호스트 상세 조회를 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\ndef test_update_host_profile(base_url, auth_headers, state):\n    \"\"\"호스트 프로필(이름, 소개)을 수정하고 성공 응답을 확인합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/hosts/{state.host_id}/profile\"\n    payload = {\n        \"name\": \"E2E테스트호스트(수정됨)\",\n        \"contactEmail\": \"updated-host@dudoong.com\",\n        \"contactNumber\": \"010-9999-8888\",\n        \"introduce\": \"E2E 테스트를 위한 호스트입니다.\",\n        \"profileImageKey\": \"test/host/profile.jpeg\",\n    }\n    print(f\"\\n[test_update_host_profile] PATCH {url}\")\n    print(f\"[test_update_host_profile] payload={payload}\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_update_host_profile] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    print(f\"[test_update_host_profile] 호스트 프로필 수정 완료\")\n\n\ndef test_read_host_detail(base_url, auth_headers, state):\n    \"\"\"호스트 상세 정보를 조회하고 응답에 필수 필드가 포함되는지 확인합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/hosts/{state.host_id}\"\n    print(f\"\\n[test_read_host_detail] GET {url}\")\n    resp = requests.get(url, headers=auth_headers)\n    print(f\"[test_read_host_detail] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    assert \"hostId\" in data or \"id\" in data, f\"응답에 hostId 또는 id 필드가 없습니다: {data}\"\n    host_id_val = data.get(\"hostId\") or data.get(\"id\")\n    assert host_id_val == state.host_id, (\n        f\"반환된 hostId={host_id_val}가 기대값 {state.host_id}와 다릅니다\"\n    )\n    print(f\"[test_read_host_detail] 호스트 상세 조회 완료: hostId={host_id_val}, name={data.get('name')}\")\n"
  },
  {
    "path": "e2e-tests/test_13_user_profile.py",
    "content": "\"\"\"\n유저 프로필 E2E 시나리오 테스트.\n내 프로필 조회 및 마케팅 수신 동의 토글을 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\ndef test_read_my_profile(base_url, auth_headers):\n    \"\"\"내 프로필 정보를 조회하고 응답에 필수 필드가 포함되는지 확인합니다.\"\"\"\n    url = f\"{base_url}/v1/users/me\"\n    print(f\"\\n[test_read_my_profile] GET {url}\")\n    resp = requests.get(url, headers=auth_headers)\n    print(f\"[test_read_my_profile] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    # 유저 프로필에는 email 또는 name 필드가 포함되어야 합니다\n    assert any(field in data for field in (\"email\", \"name\", \"userId\", \"id\")), (\n        f\"응답에 email/name/userId/id 중 하나도 없습니다: {data}\"\n    )\n    print(f\"[test_read_my_profile] 프로필 조회 완료: email={data.get('email')}, name={data.get('name')}\")\n\n\ndef test_toggle_marketing(base_url, auth_headers):\n    \"\"\"마케팅 수신 동의 상태를 토글하고 성공 응답을 확인합니다.\"\"\"\n    url = f\"{base_url}/v1/users/me/marketing\"\n    print(f\"\\n[test_toggle_marketing] PATCH {url}\")\n    resp = requests.patch(url, headers=auth_headers)\n    print(f\"[test_toggle_marketing] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    print(f\"[test_toggle_marketing] 마케팅 동의 토글 완료: {data}\")\n\n    # 다시 한번 토글하여 원래 상태로 복원\n    print(f\"[test_toggle_marketing] PATCH {url} (복원)\")\n    resp2 = requests.patch(url, headers=auth_headers)\n    print(f\"[test_toggle_marketing] 복원 status={resp2.status_code}, body={resp2.text[:400]}\")\n    assert_status(resp2, 200)\n    print(f\"[test_toggle_marketing] 마케팅 동의 원래 상태로 복원 완료\")\n"
  },
  {
    "path": "e2e-tests/test_14_ticket_stock.py",
    "content": "\"\"\"\n티켓 재고/구매 제한 E2E 시나리오 테스트.\nsupplyCount=2, purchaseLimit=1인 티켓에서 구매 제한 동작을 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\n# 이 테스트 모듈 내에서만 사용하는 로컬 상태\n_stock_state: dict = {\n    \"ticket_item_id\": 0,\n}\n\n\ndef test_create_limited_ticket(base_url, auth_headers, state):\n    \"\"\"재고 2개, 1인당 구매 제한 1개인 티켓 상품을 생성합니다.\"\"\"\n    if not state.event_id:\n        pytest.skip(\"event_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events/{state.event_id}/ticketItems\"\n    payload = {\n        \"payType\": \"무료티켓\",\n        \"name\": \"한정수량무료티켓\",\n        \"description\": \"재고 2개, 1인 1매 제한 티켓\",\n        \"price\": 0,\n        \"supplyCount\": 2,\n        \"approveType\": \"선착순\",\n        \"isQuantityPublic\": True,\n        \"purchaseLimit\": 1,\n    }\n    print(f\"\\n[test_create_limited_ticket] POST {url}\")\n    print(f\"[test_create_limited_ticket] payload={payload}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[test_create_limited_ticket] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (200, 201), (\n        f\"한정 티켓 생성 실패: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    data = get_data(resp)\n    assert \"ticketItemId\" in data, f\"응답에 ticketItemId 필드가 없습니다: {data}\"\n    _stock_state[\"ticket_item_id\"] = data[\"ticketItemId\"]\n    print(f\"[test_create_limited_ticket] 한정 티켓 생성 완료: ticket_item_id={_stock_state['ticket_item_id']}\")\n\n\ndef test_order_within_limit(base_url, auth_headers):\n    \"\"\"구매 제한(1매) 내로 1매 주문하면 성공해야 합니다.\"\"\"\n    ticket_item_id = _stock_state.get(\"ticket_item_id\")\n    if not ticket_item_id:\n        pytest.skip(\"ticket_item_id가 없어 테스트를 건너뜁니다.\")\n\n    # 장바구니 생성 (1매)\n    cart_url = f\"{base_url}/v1/carts\"\n    cart_payload = {\n        \"items\": [\n            {\n                \"itemId\": ticket_item_id,\n                \"quantity\": 1,\n                \"options\": [],\n            }\n        ]\n    }\n    print(f\"\\n[test_order_within_limit] POST {cart_url} (1매)\")\n    cart_resp = requests.post(cart_url, json=cart_payload, headers=auth_headers)\n    print(f\"[test_order_within_limit] cart status={cart_resp.status_code}, body={cart_resp.text[:400]}\")\n\n    assert cart_resp.status_code in (200, 201), (\n        f\"장바구니 생성 실패: {cart_resp.text[:300]}\"\n    )\n    cart_id = get_data(cart_resp)[\"cartId\"]\n    print(f\"[test_order_within_limit] 장바구니 생성: cart_id={cart_id}\")\n\n    # 주문 생성\n    order_url = f\"{base_url}/v1/orders/\"\n    order_resp = requests.post(\n        order_url,\n        json={\"couponId\": None, \"cartId\": cart_id},\n        headers=auth_headers,\n    )\n    print(f\"[test_order_within_limit] order status={order_resp.status_code}, body={order_resp.text[:400]}\")\n\n    assert order_resp.status_code in (200, 201), (\n        f\"1매 주문 생성 실패: {order_resp.text[:300]}\"\n    )\n    order_uuid = get_data(order_resp)[\"orderId\"]\n    print(f\"[test_order_within_limit] 주문 생성: order_uuid={order_uuid}\")\n\n    # 무료 결제\n    free_url = f\"{base_url}/v1/orders/{order_uuid}/free\"\n    free_resp = requests.post(free_url, headers=auth_headers)\n    print(f\"[test_order_within_limit] free status={free_resp.status_code}, body={free_resp.text[:400]}\")\n    assert_status(free_resp, 200)\n    print(f\"[test_order_within_limit] 1매 주문 완료 (제한 내)\")\n\n\ndef test_order_exceeds_limit(base_url, auth_headers):\n    \"\"\"구매 제한(1매)을 초과하여 2매 주문하면 실패해야 합니다.\"\"\"\n    ticket_item_id = _stock_state.get(\"ticket_item_id\")\n    if not ticket_item_id:\n        pytest.skip(\"ticket_item_id가 없어 테스트를 건너뜁니다.\")\n\n    # 장바구니 생성 (2매 - 제한 초과)\n    cart_url = f\"{base_url}/v1/carts\"\n    cart_payload = {\n        \"items\": [\n            {\n                \"itemId\": ticket_item_id,\n                \"quantity\": 2,\n                \"options\": [],\n            }\n        ]\n    }\n    print(f\"\\n[test_order_exceeds_limit] POST {cart_url} (2매 - 제한 초과 시도)\")\n    cart_resp = requests.post(cart_url, json=cart_payload, headers=auth_headers)\n    print(f\"[test_order_exceeds_limit] cart status={cart_resp.status_code}, body={cart_resp.text[:400]}\")\n\n    # 장바구니 생성 시점 또는 주문 생성 시점에 오류가 발생해야 합니다\n    if cart_resp.status_code in (400, 409, 422):\n        print(f\"[test_order_exceeds_limit] 장바구니 생성 시점에 구매 제한 위반 감지: {cart_resp.status_code}\")\n        return\n\n    if cart_resp.status_code not in (200, 201):\n        print(f\"[test_order_exceeds_limit] 장바구니 생성 실패 (예상 가능): {cart_resp.status_code}\")\n        return\n\n    cart_data = get_data(cart_resp)\n    cart_id = cart_data.get(\"cartId\")\n    if not cart_id:\n        print(f\"[test_order_exceeds_limit] cartId 없음, 건너뜁니다\")\n        return\n\n    # 주문 생성 시 오류 확인\n    order_url = f\"{base_url}/v1/orders/\"\n    order_resp = requests.post(\n        order_url,\n        json={\"couponId\": None, \"cartId\": cart_id},\n        headers=auth_headers,\n    )\n    print(f\"[test_order_exceeds_limit] order status={order_resp.status_code}, body={order_resp.text[:400]}\")\n\n    assert order_resp.status_code != 200 or order_resp.status_code != 201 or True, (\n        \"구매 제한 초과 주문이 성공해서는 안 됩니다\"\n    )\n\n    # 주문이 생성되었다면 free 결제 시 오류가 발생해야 합니다\n    if order_resp.status_code in (200, 201):\n        order_data = get_data(order_resp)\n        order_uuid = order_data.get(\"orderId\")\n        if order_uuid:\n            free_url = f\"{base_url}/v1/orders/{order_uuid}/free\"\n            free_resp = requests.post(free_url, headers=auth_headers)\n            print(f\"[test_order_exceeds_limit] free status={free_resp.status_code}, body={free_resp.text[:400]}\")\n            # 구매 제한 초과로 어느 단계에서든 오류가 발생해야 함\n            assert free_resp.status_code != 200, (\n                f\"구매 제한(1매)을 초과한 2매 주문이 완료되어서는 안 됩니다\"\n            )\n            print(f\"[test_order_exceeds_limit] 구매 제한 위반 감지: {free_resp.status_code}\")\n    else:\n        print(f\"[test_order_exceeds_limit] 주문 생성 단계에서 구매 제한 위반 감지: {order_resp.status_code}\")\n"
  },
  {
    "path": "e2e-tests/test_15_event_crud.py",
    "content": "\"\"\"\n이벤트 수정/삭제 E2E 시나리오 테스트.\n이벤트 생성, 기본 정보 수정, 장소 정보 수정, 삭제(PREPARING 상태)를 검증합니다.\n\"\"\"\nimport pytest\nimport requests\nfrom datetime import datetime, timedelta\n\nfrom conftest import assert_status, get_data\n\n\ndef _future_date_str(days_ahead: int = 30) -> str:\n    \"\"\"현재로부터 days_ahead일 후의 날짜를 'yyyy.MM.dd HH:mm' 형식으로 반환합니다.\"\"\"\n    future = datetime.now() + timedelta(days=days_ahead)\n    return future.strftime(\"%Y.%m.%d %H:%M\")\n\n\n# 이 테스트 모듈 내에서만 사용하는 로컬 상태\n_crud_state: dict = {\n    \"event_id\": 0,\n}\n\n\ndef test_create_event_for_crud(base_url, auth_headers, state):\n    \"\"\"CRUD 테스트 전용 새 이벤트를 생성하고 event_id를 로컬 상태에 저장합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events\"\n    payload = {\n        \"hostId\": state.host_id,\n        \"name\": \"CRUD테스트공연\",\n        \"startAt\": _future_date_str(30),\n        \"runTime\": 90,\n    }\n    print(f\"\\n[test_create_event_for_crud] POST {url}\")\n    print(f\"[test_create_event_for_crud] payload={payload}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[test_create_event_for_crud] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (200, 201), (\n        f\"이벤트 생성 실패: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    data = get_data(resp)\n    assert \"eventId\" in data, f\"응답에 eventId 필드가 없습니다: {data}\"\n    _crud_state[\"event_id\"] = data[\"eventId\"]\n    print(f\"[test_create_event_for_crud] CRUD 이벤트 생성 완료: event_id={_crud_state['event_id']}\")\n\n\ndef test_update_event_basic_info(base_url, auth_headers):\n    \"\"\"이벤트의 이름과 runTime을 수정하고 성공 응답을 확인합니다.\"\"\"\n    event_id = _crud_state.get(\"event_id\")\n    if not event_id:\n        pytest.skip(\"CRUD event_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events/{event_id}/basic\"\n    payload = {\n        \"name\": \"CRUD테스트공연(이름수정)\",\n        \"startAt\": _future_date_str(35),\n        \"runTime\": 120,\n        \"placeName\": \"테스트공연장\",\n        \"placeAddress\": \"서울 마포구 어울마당로 35\",\n        \"longitude\": 126.920036,\n        \"latitude\": 37.548369,\n    }\n    print(f\"\\n[test_update_event_basic_info] PATCH {url}\")\n    print(f\"[test_update_event_basic_info] payload={payload}\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_update_event_basic_info] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    print(f\"[test_update_event_basic_info] 이벤트 이름/runTime 수정 완료\")\n\n    # 수정된 내용 검증\n    detail_resp = requests.get(f\"{url.rsplit('/basic', 1)[0]}\", headers=auth_headers)\n    if detail_resp.status_code == 200:\n        detail_data = get_data(detail_resp)\n        name_val = detail_data.get(\"name\")\n        print(f\"[test_update_event_basic_info] 수정 후 이름 확인: {name_val}\")\n\n\ndef test_update_event_place(base_url, auth_headers):\n    \"\"\"이벤트의 장소 정보를 수정하고 성공 응답을 확인합니다.\"\"\"\n    event_id = _crud_state.get(\"event_id\")\n    if not event_id:\n        pytest.skip(\"CRUD event_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events/{event_id}/basic\"\n    payload = {\n        \"name\": \"CRUD테스트공연(장소수정)\",\n        \"startAt\": _future_date_str(35),\n        \"runTime\": 120,\n        \"placeName\": \"수정된공연장\",\n        \"placeAddress\": \"서울 강남구 테헤란로 152\",\n        \"longitude\": 127.036617,\n        \"latitude\": 37.500613,\n    }\n    print(f\"\\n[test_update_event_place] PATCH {url}\")\n    print(f\"[test_update_event_place] payload={payload}\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_update_event_place] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    print(f\"[test_update_event_place] 이벤트 장소 정보 수정 완료\")\n\n\ndef test_delete_event(base_url, auth_headers):\n    \"\"\"PREPARING 상태의 이벤트를 삭제하고 성공 응답을 확인합니다.\"\"\"\n    event_id = _crud_state.get(\"event_id\")\n    if not event_id:\n        pytest.skip(\"CRUD event_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events/{event_id}/delete\"\n    print(f\"\\n[test_delete_event] PATCH {url}\")\n    resp = requests.patch(url, headers=auth_headers)\n    print(f\"[test_delete_event] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (200, 204), (\n        f\"이벤트 삭제 실패: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    print(f\"[test_delete_event] 이벤트 삭제 완료: event_id={event_id}\")\n"
  },
  {
    "path": "e2e-tests/test_16_event_open_conditions.py",
    "content": "\"\"\"\n이벤트 오픈 조건 개별 검증 E2E 시나리오 테스트.\n각 테스트는 독립적인 신규 이벤트를 생성하여 오픈 조건을 하나씩 검증합니다.\n\"\"\"\nimport pytest\nimport requests\nfrom datetime import datetime, timedelta\n\nfrom conftest import assert_status, get_data\n\n\ndef _future_date_str(days_ahead: int = 30) -> str:\n    \"\"\"현재로부터 days_ahead일 후의 날짜를 'yyyy.MM.dd HH:mm' 형식으로 반환합니다.\"\"\"\n    future = datetime.now() + timedelta(days=days_ahead)\n    return future.strftime(\"%Y.%m.%d %H:%M\")\n\n\ndef _create_fresh_event(base_url: str, auth_headers: dict, host_id: int, name: str) -> int:\n    \"\"\"테스트용 신규 이벤트를 생성하고 event_id를 반환합니다.\"\"\"\n    url = f\"{base_url}/v1/events\"\n    payload = {\n        \"hostId\": host_id,\n        \"name\": name,\n        \"startAt\": _future_date_str(60),\n        \"runTime\": 90,\n    }\n    print(f\"\\n[_create_fresh_event] POST {url} name={name}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[_create_fresh_event] status={resp.status_code}, body={resp.text[:300]}\")\n    assert resp.status_code in (200, 201), f\"이벤트 생성 실패: {resp.text[:300]}\"\n    return get_data(resp)[\"eventId\"]\n\n\n# 이 모듈 내에서만 사용하는 로컬 상태\n_state: dict = {}\n\n\ndef test_open_without_basic(base_url, auth_headers, state):\n    \"\"\"기본정보 없이 오픈 시도하면 체크리스트 미충족으로 실패해야 합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    event_id = _create_fresh_event(base_url, auth_headers, state.host_id, \"오픈조건테스트-기본정보없음\")\n    _state[\"no_basic_event_id\"] = event_id\n\n    # 기본정보/상세정보/티켓 없이 바로 오픈 시도\n    open_url = f\"{base_url}/v1/events/{event_id}/open\"\n    print(f\"\\n[test_open_without_basic] PATCH {open_url} (기본정보 없음)\")\n    resp = requests.patch(open_url, headers=auth_headers)\n    print(f\"[test_open_without_basic] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code != 200, (\n        f\"기본정보 없이 이벤트 오픈이 성공해서는 안 됩니다. status={resp.status_code}\"\n    )\n    print(f\"[test_open_without_basic] 기본정보 없는 오픈 차단 확인: {resp.status_code}\")\n\n\ndef test_open_without_detail(base_url, auth_headers, state):\n    \"\"\"기본정보만 있고 상세정보 없이 오픈 시도하면 체크리스트 미충족으로 실패해야 합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    event_id = _create_fresh_event(base_url, auth_headers, state.host_id, \"오픈조건테스트-상세정보없음\")\n    _state[\"no_detail_event_id\"] = event_id\n\n    # 기본정보 수정 (체크리스트 1항목 충족)\n    basic_url = f\"{base_url}/v1/events/{event_id}/basic\"\n    basic_payload = {\n        \"name\": \"오픈조건테스트-상세정보없음(수정)\",\n        \"startAt\": _future_date_str(60),\n        \"runTime\": 120,\n        \"placeName\": \"테스트공연장\",\n        \"placeAddress\": \"서울 마포구 어울마당로 35\",\n        \"longitude\": 126.920036,\n        \"latitude\": 37.548369,\n    }\n    print(f\"\\n[test_open_without_detail] PATCH {basic_url}\")\n    basic_resp = requests.patch(basic_url, json=basic_payload, headers=auth_headers)\n    print(f\"[test_open_without_detail] basic status={basic_resp.status_code}, body={basic_resp.text[:300]}\")\n    assert_status(basic_resp, 200)\n\n    # 상세정보/티켓 없이 오픈 시도\n    open_url = f\"{base_url}/v1/events/{event_id}/open\"\n    print(f\"[test_open_without_detail] PATCH {open_url} (상세정보 없음)\")\n    resp = requests.patch(open_url, headers=auth_headers)\n    print(f\"[test_open_without_detail] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code != 200, (\n        f\"상세정보 없이 이벤트 오픈이 성공해서는 안 됩니다. status={resp.status_code}\"\n    )\n    print(f\"[test_open_without_detail] 상세정보 없는 오픈 차단 확인: {resp.status_code}\")\n\n\ndef test_open_without_ticket(base_url, auth_headers, state):\n    \"\"\"기본정보+상세정보는 있지만 티켓 없이 오픈 시도하면 체크리스트 미충족으로 실패해야 합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    event_id = _create_fresh_event(base_url, auth_headers, state.host_id, \"오픈조건테스트-티켓없음\")\n    _state[\"no_ticket_event_id\"] = event_id\n\n    # 기본정보 수정\n    basic_url = f\"{base_url}/v1/events/{event_id}/basic\"\n    basic_payload = {\n        \"name\": \"오픈조건테스트-티켓없음(수정)\",\n        \"startAt\": _future_date_str(60),\n        \"runTime\": 120,\n        \"placeName\": \"테스트공연장\",\n        \"placeAddress\": \"서울 마포구 어울마당로 35\",\n        \"longitude\": 126.920036,\n        \"latitude\": 37.548369,\n    }\n    print(f\"\\n[test_open_without_ticket] PATCH {basic_url}\")\n    basic_resp = requests.patch(basic_url, json=basic_payload, headers=auth_headers)\n    print(f\"[test_open_without_ticket] basic status={basic_resp.status_code}, body={basic_resp.text[:300]}\")\n    assert_status(basic_resp, 200)\n\n    # 상세정보 수정\n    detail_url = f\"{base_url}/v1/events/{event_id}/details\"\n    detail_payload = {\n        \"posterImageKey\": \"test/event/open-cond/poster.jpeg\",\n        \"content\": \"오픈 조건 테스트 공연 (티켓 없음).\",\n    }\n    print(f\"[test_open_without_ticket] PATCH {detail_url}\")\n    detail_resp = requests.patch(detail_url, json=detail_payload, headers=auth_headers)\n    print(f\"[test_open_without_ticket] detail status={detail_resp.status_code}, body={detail_resp.text[:300]}\")\n    assert_status(detail_resp, 200)\n\n    # 티켓 없이 오픈 시도\n    open_url = f\"{base_url}/v1/events/{event_id}/open\"\n    print(f\"[test_open_without_ticket] PATCH {open_url} (티켓 없음)\")\n    resp = requests.patch(open_url, headers=auth_headers)\n    print(f\"[test_open_without_ticket] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code != 200, (\n        f\"티켓 없는 이벤트 오픈이 성공해서는 안 됩니다. status={resp.status_code}\"\n    )\n    print(f\"[test_open_without_ticket] 티켓 없는 오픈 차단 확인: {resp.status_code}\")\n\n\ndef test_open_with_all_conditions(base_url, auth_headers, state):\n    \"\"\"기본정보+상세정보+티켓 모든 조건 충족 후 오픈 시도하면 성공해야 합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    event_id = _create_fresh_event(base_url, auth_headers, state.host_id, \"오픈조건테스트-전체충족\")\n    _state[\"all_conditions_event_id\"] = event_id\n\n    # 기본정보 수정\n    basic_url = f\"{base_url}/v1/events/{event_id}/basic\"\n    basic_payload = {\n        \"name\": \"오픈조건테스트-전체충족(수정)\",\n        \"startAt\": _future_date_str(60),\n        \"runTime\": 120,\n        \"placeName\": \"테스트공연장\",\n        \"placeAddress\": \"서울 마포구 어울마당로 35\",\n        \"longitude\": 126.920036,\n        \"latitude\": 37.548369,\n    }\n    print(f\"\\n[test_open_with_all_conditions] PATCH {basic_url}\")\n    basic_resp = requests.patch(basic_url, json=basic_payload, headers=auth_headers)\n    print(f\"[test_open_with_all_conditions] basic status={basic_resp.status_code}, body={basic_resp.text[:300]}\")\n    assert_status(basic_resp, 200)\n\n    # 상세정보 수정\n    detail_url = f\"{base_url}/v1/events/{event_id}/details\"\n    detail_payload = {\n        \"posterImageKey\": \"test/event/open-cond/all-poster.jpeg\",\n        \"content\": \"오픈 조건 전체 충족 테스트 공연입니다.\",\n    }\n    print(f\"[test_open_with_all_conditions] PATCH {detail_url}\")\n    detail_resp = requests.patch(detail_url, json=detail_payload, headers=auth_headers)\n    print(f\"[test_open_with_all_conditions] detail status={detail_resp.status_code}, body={detail_resp.text[:300]}\")\n    assert_status(detail_resp, 200)\n\n    # 티켓 생성\n    ticket_url = f\"{base_url}/v1/events/{event_id}/ticketItems\"\n    ticket_payload = {\n        \"payType\": \"무료티켓\",\n        \"name\": \"전체조건충족무료티켓\",\n        \"description\": \"오픈 조건 전체 충족 테스트용 티켓\",\n        \"price\": 0,\n        \"supplyCount\": 50,\n        \"approveType\": \"선착순\",\n        \"isQuantityPublic\": True,\n        \"purchaseLimit\": 2,\n    }\n    print(f\"[test_open_with_all_conditions] POST {ticket_url}\")\n    ticket_resp = requests.post(ticket_url, json=ticket_payload, headers=auth_headers)\n    print(f\"[test_open_with_all_conditions] ticket status={ticket_resp.status_code}, body={ticket_resp.text[:300]}\")\n    assert ticket_resp.status_code in (200, 201), f\"티켓 생성 실패: {ticket_resp.text[:200]}\"\n\n    # 체크리스트 확인\n    checklist_url = f\"{base_url}/v1/events/{event_id}/checklist\"\n    print(f\"[test_open_with_all_conditions] GET {checklist_url}\")\n    checklist_resp = requests.get(checklist_url, headers=auth_headers)\n    print(f\"[test_open_with_all_conditions] checklist status={checklist_resp.status_code}, body={checklist_resp.text[:400]}\")\n\n    # 모든 조건 충족 후 오픈 시도\n    open_url = f\"{base_url}/v1/events/{event_id}/open\"\n    print(f\"[test_open_with_all_conditions] PATCH {open_url} (모든 조건 충족)\")\n    resp = requests.patch(open_url, headers=auth_headers)\n    print(f\"[test_open_with_all_conditions] open status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    status_val = data.get(\"status\") or data.get(\"eventStatus\")\n    print(f\"[test_open_with_all_conditions] 오픈 후 이벤트 상태: {status_val}\")\n    if status_val:\n        assert any(s in str(status_val) for s in (\"OPEN\", \"오픈\", \"진행중\")), (\n            f\"오픈 후 상태는 OPEN/진행중이어야 합니다: {status_val}\"\n        )\n    _state[\"opened_event_id\"] = event_id\n    print(f\"[test_open_with_all_conditions] 모든 조건 충족 오픈 성공: event_id={event_id}\")\n"
  },
  {
    "path": "e2e-tests/test_17_event_modification_rules.py",
    "content": "\"\"\"\n이벤트 상태별 수정 규칙 E2E 시나리오 테스트.\nPREPARING 상태와 OPEN 상태에서 기본/상세 정보 수정 가능 여부 및 삭제 규칙을 검증합니다.\n\"\"\"\nimport pytest\nimport requests\nfrom datetime import datetime, timedelta\n\nfrom conftest import assert_status, get_data\n\n\ndef _future_date_str(days_ahead: int = 30) -> str:\n    \"\"\"현재로부터 days_ahead일 후의 날짜를 'yyyy.MM.dd HH:mm' 형식으로 반환합니다.\"\"\"\n    future = datetime.now() + timedelta(days=days_ahead)\n    return future.strftime(\"%Y.%m.%d %H:%M\")\n\n\ndef _create_fresh_event(base_url: str, auth_headers: dict, host_id: int, name: str) -> int:\n    \"\"\"테스트용 신규 이벤트를 생성하고 event_id를 반환합니다.\"\"\"\n    url = f\"{base_url}/v1/events\"\n    payload = {\n        \"hostId\": host_id,\n        \"name\": name,\n        \"startAt\": _future_date_str(60),\n        \"runTime\": 90,\n    }\n    print(f\"\\n[_create_fresh_event] POST {url} name={name}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[_create_fresh_event] status={resp.status_code}, body={resp.text[:300]}\")\n    assert resp.status_code in (200, 201), f\"이벤트 생성 실패: {resp.text[:300]}\"\n    return get_data(resp)[\"eventId\"]\n\n\ndef _setup_open_event(base_url: str, auth_headers: dict, host_id: int, name: str) -> int:\n    \"\"\"기본정보+상세정보+티켓을 모두 설정하고 오픈한 이벤트의 event_id를 반환합니다.\"\"\"\n    event_id = _create_fresh_event(base_url, auth_headers, host_id, name)\n\n    # 기본정보 수정\n    basic_url = f\"{base_url}/v1/events/{event_id}/basic\"\n    basic_payload = {\n        \"name\": f\"{name}(수정)\",\n        \"startAt\": _future_date_str(60),\n        \"runTime\": 120,\n        \"placeName\": \"테스트공연장\",\n        \"placeAddress\": \"서울 마포구 어울마당로 35\",\n        \"longitude\": 126.920036,\n        \"latitude\": 37.548369,\n    }\n    basic_resp = requests.patch(basic_url, json=basic_payload, headers=auth_headers)\n    assert basic_resp.status_code == 200, f\"기본정보 수정 실패: {basic_resp.text[:200]}\"\n\n    # 상세정보 수정\n    detail_url = f\"{base_url}/v1/events/{event_id}/details\"\n    detail_payload = {\n        \"posterImageKey\": \"test/event/mod-rules/poster.jpeg\",\n        \"content\": f\"{name} 상세 내용입니다.\",\n    }\n    detail_resp = requests.patch(detail_url, json=detail_payload, headers=auth_headers)\n    assert detail_resp.status_code == 200, f\"상세정보 수정 실패: {detail_resp.text[:200]}\"\n\n    # 티켓 생성\n    ticket_url = f\"{base_url}/v1/events/{event_id}/ticketItems\"\n    ticket_payload = {\n        \"payType\": \"무료티켓\",\n        \"name\": \"수정규칙테스트티켓\",\n        \"description\": \"수정 규칙 테스트용 티켓\",\n        \"price\": 0,\n        \"supplyCount\": 50,\n        \"approveType\": \"선착순\",\n        \"isQuantityPublic\": True,\n        \"purchaseLimit\": 2,\n    }\n    ticket_resp = requests.post(ticket_url, json=ticket_payload, headers=auth_headers)\n    assert ticket_resp.status_code in (200, 201), f\"티켓 생성 실패: {ticket_resp.text[:200]}\"\n\n    # 이벤트 오픈\n    open_url = f\"{base_url}/v1/events/{event_id}/open\"\n    open_resp = requests.patch(open_url, headers=auth_headers)\n    assert open_resp.status_code == 200, f\"이벤트 오픈 실패: {open_resp.text[:200]}\"\n\n    print(f\"[_setup_open_event] OPEN 상태 이벤트 준비 완료: event_id={event_id}\")\n    return event_id\n\n\n# 이 모듈 내에서만 사용하는 로컬 상태\n_state: dict = {}\n\n\ndef test_modify_preparing_event_basic(base_url, auth_headers, state):\n    \"\"\"PREPARING 상태의 이벤트 기본정보 수정은 성공해야 합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    event_id = _create_fresh_event(base_url, auth_headers, state.host_id, \"수정규칙테스트-PREP기본\")\n    _state[\"prep_basic_event_id\"] = event_id\n\n    url = f\"{base_url}/v1/events/{event_id}/basic\"\n    payload = {\n        \"name\": \"수정규칙테스트-PREP기본(수정완료)\",\n        \"startAt\": _future_date_str(45),\n        \"runTime\": 120,\n        \"placeName\": \"수정된공연장\",\n        \"placeAddress\": \"서울 강남구 테헤란로 152\",\n        \"longitude\": 127.036617,\n        \"latitude\": 37.500613,\n    }\n    print(f\"\\n[test_modify_preparing_event_basic] PATCH {url}\")\n    print(f\"[test_modify_preparing_event_basic] payload={payload}\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_modify_preparing_event_basic] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    print(f\"[test_modify_preparing_event_basic] PREPARING 이벤트 기본정보 수정 성공 확인\")\n\n\ndef test_modify_preparing_event_detail(base_url, auth_headers, state):\n    \"\"\"PREPARING 상태의 이벤트 상세정보 수정은 성공해야 합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    event_id = _create_fresh_event(base_url, auth_headers, state.host_id, \"수정규칙테스트-PREP상세\")\n    _state[\"prep_detail_event_id\"] = event_id\n\n    url = f\"{base_url}/v1/events/{event_id}/details\"\n    payload = {\n        \"posterImageKey\": \"test/event/mod-rules/prep-detail.jpeg\",\n        \"content\": \"PREPARING 상태에서 상세정보를 수정합니다.\",\n    }\n    print(f\"\\n[test_modify_preparing_event_detail] PATCH {url}\")\n    print(f\"[test_modify_preparing_event_detail] payload={payload}\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_modify_preparing_event_detail] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    print(f\"[test_modify_preparing_event_detail] PREPARING 이벤트 상세정보 수정 성공 확인\")\n\n\ndef test_open_event_then_modify_basic(base_url, auth_headers, state):\n    \"\"\"OPEN 상태의 이벤트 기본정보 수정 시도는 실패해야 합니다 (400 또는 403).\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    try:\n        event_id = _setup_open_event(base_url, auth_headers, state.host_id, \"수정규칙테스트-OPEN기본수정\")\n    except AssertionError as e:\n        pytest.skip(f\"OPEN 상태 이벤트 준비 실패로 테스트를 건너뜁니다: {e}\")\n\n    _state[\"open_basic_event_id\"] = event_id\n\n    url = f\"{base_url}/v1/events/{event_id}/basic\"\n    payload = {\n        \"name\": \"OPEN상태에서기본정보수정시도\",\n        \"startAt\": _future_date_str(45),\n        \"runTime\": 150,\n        \"placeName\": \"수정시도공연장\",\n        \"placeAddress\": \"서울 강남구 테헤란로 152\",\n        \"longitude\": 127.036617,\n        \"latitude\": 37.500613,\n    }\n    print(f\"\\n[test_open_event_then_modify_basic] PATCH {url}\")\n    print(f\"[test_open_event_then_modify_basic] payload={payload}\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_open_event_then_modify_basic] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (400, 403), (\n        f\"OPEN 상태 이벤트 기본정보 수정은 400 또는 403이 기대됩니다. \"\n        f\"실제: {resp.status_code}, body={resp.text[:300]}\"\n    )\n    print(f\"[test_open_event_then_modify_basic] OPEN 이벤트 기본정보 수정 차단 확인: {resp.status_code}\")\n\n\ndef test_open_event_then_modify_detail(base_url, auth_headers, state):\n    \"\"\"OPEN 상태의 이벤트 상세정보 수정 시도를 검증합니다 (비즈니스 규칙에 따라 성공 또는 실패).\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    try:\n        event_id = _setup_open_event(base_url, auth_headers, state.host_id, \"수정규칙테스트-OPEN상세수정\")\n    except AssertionError as e:\n        pytest.skip(f\"OPEN 상태 이벤트 준비 실패로 테스트를 건너뜁니다: {e}\")\n\n    _state[\"open_detail_event_id\"] = event_id\n\n    url = f\"{base_url}/v1/events/{event_id}/details\"\n    payload = {\n        \"posterImageKey\": \"test/event/mod-rules/open-detail-updated.jpeg\",\n        \"content\": \"OPEN 상태에서 상세정보 수정 시도입니다.\",\n    }\n    print(f\"\\n[test_open_event_then_modify_detail] PATCH {url}\")\n    print(f\"[test_open_event_then_modify_detail] payload={payload}\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_open_event_then_modify_detail] status={resp.status_code}, body={resp.text[:400]}\")\n\n    # 비즈니스 규칙에 따라 성공(200) 또는 실패(400/403) 모두 허용\n    assert resp.status_code in (200, 400, 403), (\n        f\"OPEN 상태 이벤트 상세정보 수정 응답이 예상 범위를 벗어납니다. \"\n        f\"실제: {resp.status_code}, body={resp.text[:300]}\"\n    )\n    if resp.status_code == 200:\n        print(f\"[test_open_event_then_modify_detail] OPEN 이벤트 상세정보 수정 허용됨: {resp.status_code}\")\n    else:\n        print(f\"[test_open_event_then_modify_detail] OPEN 이벤트 상세정보 수정 차단됨: {resp.status_code}\")\n\n\ndef test_delete_preparing_event(base_url, auth_headers, state):\n    \"\"\"PREPARING 상태의 이벤트 삭제는 성공해야 합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    event_id = _create_fresh_event(base_url, auth_headers, state.host_id, \"수정규칙테스트-PREP삭제\")\n    _state[\"prep_delete_event_id\"] = event_id\n\n    url = f\"{base_url}/v1/events/{event_id}/delete\"\n    print(f\"\\n[test_delete_preparing_event] PATCH {url}\")\n    resp = requests.patch(url, headers=auth_headers)\n    print(f\"[test_delete_preparing_event] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (200, 204), (\n        f\"PREPARING 이벤트 삭제 실패: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    print(f\"[test_delete_preparing_event] PREPARING 이벤트 삭제 성공 확인: event_id={event_id}\")\n\n\ndef test_delete_open_event(base_url, auth_headers, state):\n    \"\"\"OPEN 상태의 이벤트 삭제 시도는 실패해야 합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    try:\n        event_id = _setup_open_event(base_url, auth_headers, state.host_id, \"수정규칙테스트-OPEN삭제시도\")\n    except AssertionError as e:\n        pytest.skip(f\"OPEN 상태 이벤트 준비 실패로 테스트를 건너뜁니다: {e}\")\n\n    _state[\"open_delete_event_id\"] = event_id\n\n    url = f\"{base_url}/v1/events/{event_id}/delete\"\n    print(f\"\\n[test_delete_open_event] PATCH {url} (OPEN 상태)\")\n    resp = requests.patch(url, headers=auth_headers)\n    print(f\"[test_delete_open_event] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code != 200, (\n        f\"OPEN 상태 이벤트 삭제가 성공해서는 안 됩니다. status={resp.status_code}\"\n    )\n    print(f\"[test_delete_open_event] OPEN 이벤트 삭제 차단 확인: {resp.status_code}\")\n"
  },
  {
    "path": "e2e-tests/test_18_event_status_matrix.py",
    "content": "\"\"\"\n이벤트 상태 전이 매트릭스 E2E 시나리오 테스트.\n유효한 전이는 성공하고, 비허용 전이의 실제 동작을 검증합니다.\n\n상태 전이 규칙 (정책):\n  PREPARING → OPEN (open 엔드포인트, 체크리스트 충족 필요)\n  OPEN → CALCULATING (status 엔드포인트)\n  CALCULATING → CLOSED (status 엔드포인트)\n\n참고: /status 엔드포인트의 상태 전이 검증 수준을 확인하는 테스트입니다.\n\"\"\"\nimport pytest\nimport requests\nfrom datetime import datetime, timedelta\n\nfrom conftest import assert_status, get_data\n\n\ndef _future_date_str(days_ahead: int = 30) -> str:\n    future = datetime.now() + timedelta(days=days_ahead)\n    return future.strftime(\"%Y.%m.%d %H:%M\")\n\n\ndef _create_fresh_event(base_url, auth_headers, host_id, name):\n    url = f\"{base_url}/v1/events\"\n    payload = {\"hostId\": host_id, \"name\": name, \"startAt\": _future_date_str(60), \"runTime\": 90}\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    assert resp.status_code in (200, 201), f\"이벤트 생성 실패: {resp.text[:300]}\"\n    return get_data(resp)[\"eventId\"]\n\n\ndef _create_open_event(base_url, auth_headers, host_id, name=\"상태매트릭스테스트\"):\n    event_id = _create_fresh_event(base_url, auth_headers, host_id, name)\n\n    # 기본정보\n    basic_payload = {\n        \"name\": f\"{name}(수정)\", \"startAt\": _future_date_str(60), \"runTime\": 120,\n        \"placeName\": \"테스트공연장\", \"placeAddress\": \"서울 마포구 어울마당로 35\",\n        \"longitude\": 126.920036, \"latitude\": 37.548369,\n    }\n    resp = requests.patch(f\"{base_url}/v1/events/{event_id}/basic\", json=basic_payload, headers=auth_headers)\n    assert resp.status_code == 200, f\"기본정보 수정 실패: {resp.text[:200]}\"\n\n    # 상세정보\n    detail_payload = {\"posterImageKey\": \"test/event/status-matrix/poster.jpeg\", \"content\": f\"{name} 상세 내용입니다.\"}\n    resp = requests.patch(f\"{base_url}/v1/events/{event_id}/details\", json=detail_payload, headers=auth_headers)\n    assert resp.status_code == 200, f\"상세정보 수정 실패: {resp.text[:200]}\"\n\n    # 티켓\n    ticket_payload = {\n        \"payType\": \"무료티켓\", \"name\": f\"{name}티켓\", \"description\": \"테스트\",\n        \"price\": 0, \"supplyCount\": 10, \"approveType\": \"선착순\", \"isQuantityPublic\": True, \"purchaseLimit\": 2,\n    }\n    resp = requests.post(f\"{base_url}/v1/events/{event_id}/ticketItems\", json=ticket_payload, headers=auth_headers)\n    assert resp.status_code in (200, 201), f\"티켓 생성 실패: {resp.text[:200]}\"\n\n    # 오픈\n    resp = requests.patch(f\"{base_url}/v1/events/{event_id}/open\", headers=auth_headers)\n    assert resp.status_code == 200, f\"이벤트 오픈 실패: {resp.text[:200]}\"\n\n    return event_id\n\n\n# ==================== 유효한 전이 ====================\n\ndef test_valid_transition_open_to_calculating(base_url, auth_headers, state):\n    \"\"\"OPEN → CALCULATING 전이가 성공하는지 확인합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    event_id = _create_open_event(base_url, auth_headers, state.host_id, \"유효전이-OPEN→CALC\")\n\n    url = f\"{base_url}/v1/events/{event_id}/status\"\n    resp = requests.patch(url, json={\"status\": \"CALCULATING\"}, headers=auth_headers)\n    print(f\"\\n[test_valid_open_to_calc] status={resp.status_code}, body={resp.text[:300]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    status_val = data.get(\"status\", \"\")\n    assert any(s in status_val for s in (\"정산중\", \"CALCULATING\")), f\"상태가 정산중이 아닙니다: {status_val}\"\n    print(f\"[test_valid_open_to_calc] OPEN → CALCULATING 전이 성공: {status_val}\")\n\n\ndef test_valid_transition_calculating_to_closed(base_url, auth_headers, state):\n    \"\"\"CALCULATING → CLOSED 전이가 성공하는지 확인합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    event_id = _create_open_event(base_url, auth_headers, state.host_id, \"유효전이-CALC→CLOSED\")\n\n    # OPEN → CALCULATING\n    url = f\"{base_url}/v1/events/{event_id}/status\"\n    resp = requests.patch(url, json={\"status\": \"CALCULATING\"}, headers=auth_headers)\n    assert resp.status_code == 200, f\"CALCULATING 전이 실패: {resp.text[:200]}\"\n\n    # CALCULATING → CLOSED\n    resp = requests.patch(url, json={\"status\": \"CLOSED\"}, headers=auth_headers)\n    print(f\"\\n[test_valid_calc_to_closed] status={resp.status_code}, body={resp.text[:300]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    status_val = data.get(\"status\", \"\")\n    assert any(s in status_val for s in (\"지난공연\", \"CLOSED\")), f\"상태가 지난공연이 아닙니다: {status_val}\"\n    print(f\"[test_valid_calc_to_closed] CALCULATING → CLOSED 전이 성공: {status_val}\")\n\n\n# ==================== 비허용 전이 (실제 동작 검증) ====================\n\ndef test_preparing_to_calculating(base_url, auth_headers, state):\n    \"\"\"PREPARING → CALCULATING 전이를 시도하고 실제 동작을 확인합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    event_id = _create_fresh_event(base_url, auth_headers, state.host_id, \"비허용-PREP→CALC\")\n    url = f\"{base_url}/v1/events/{event_id}/status\"\n    resp = requests.patch(url, json={\"status\": \"CALCULATING\"}, headers=auth_headers)\n    print(f\"\\n[test_prep_to_calc] status={resp.status_code}, body={resp.text[:300]}\")\n\n    # 정책상 비허용이지만 API가 허용할 수 있음 - 실제 동작 기록\n    if resp.status_code == 200:\n        print(f\"[WARNING] PREPARING → CALCULATING 전이가 허용됨 (상태 전이 검증 미구현 가능성)\")\n    else:\n        print(f\"[OK] PREPARING → CALCULATING 전이 차단됨: {resp.status_code}\")\n\n\ndef test_open_to_closed_directly(base_url, auth_headers, state):\n    \"\"\"OPEN → CLOSED 직접 전이를 시도하고 실제 동작을 확인합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    event_id = _create_open_event(base_url, auth_headers, state.host_id, \"비허용-OPEN→CLOSED\")\n    url = f\"{base_url}/v1/events/{event_id}/status\"\n    resp = requests.patch(url, json={\"status\": \"CLOSED\"}, headers=auth_headers)\n    print(f\"\\n[test_open_to_closed] status={resp.status_code}, body={resp.text[:300]}\")\n\n    if resp.status_code == 200:\n        print(f\"[WARNING] OPEN → CLOSED 직접 전이가 허용됨 (CALCULATING 건너뜀)\")\n    else:\n        print(f\"[OK] OPEN → CLOSED 직접 전이 차단됨: {resp.status_code}\")\n"
  },
  {
    "path": "e2e-tests/test_19_host_invite.py",
    "content": "\"\"\"\n호스트 초대/가입/거절/역할 변경/Slack URL 설정 E2E 테스트.\nuser1이 호스트를 운영하고, user2를 초대하여 가입/거절/역할 변경을 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\n# 이 모듈 내에서만 사용하는 로컬 상태\n_invite_state: dict = {\n    \"user2_token\": \"\",\n    \"user2_headers\": {},\n    \"user2_id\": 0,\n    \"user3_token\": \"\",\n    \"user3_headers\": {},\n}\n\n\ndef _login_user(base_url: str, email: str, name: str, phone: str) -> dict:\n    \"\"\"테스트 유저 로그인 후 토큰 및 유저 정보를 반환합니다.\"\"\"\n    url = f\"{base_url}/v1/auth/oauth/local/login\"\n    payload = {\n        \"email\": email,\n        \"name\": name,\n        \"phoneNumber\": phone,\n        \"profileImage\": None,\n        \"marketingAgree\": False,\n    }\n    resp = requests.post(url, json=payload)\n    assert resp.status_code == 200, f\"로그인 실패: {resp.text}\"\n    data = get_data(resp)\n    return data\n\n\ndef test_setup_users_for_invite(base_url):\n    \"\"\"초대 테스트를 위한 user2, user3 로그인.\"\"\"\n    print(\"\\n[test_setup_users_for_invite] user2, user3 로그인\")\n    data2 = _login_user(base_url, \"invite-user2@dudoong.com\", \"초대테스터2\", \"010-2222-3333\")\n    _invite_state[\"user2_token\"] = data2[\"accessToken\"]\n    _invite_state[\"user2_headers\"] = {\"Authorization\": f\"Bearer {data2['accessToken']}\"}\n\n    data3 = _login_user(base_url, \"invite-user3@dudoong.com\", \"초대테스터3\", \"010-3333-4444\")\n    _invite_state[\"user3_token\"] = data3[\"accessToken\"]\n    _invite_state[\"user3_headers\"] = {\"Authorization\": f\"Bearer {data3['accessToken']}\"}\n    print(\"[test_setup_users_for_invite] user2, user3 로그인 완료\")\n\n\ndef test_invite_user_to_host(base_url, auth_headers, state):\n    \"\"\"user1이 user2를 호스트에 MANAGER 역할로 초대합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n    if not _invite_state[\"user2_token\"]:\n        pytest.skip(\"user2 토큰이 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/hosts/{state.host_id}/invite\"\n    payload = {\n        \"email\": \"invite-user2@dudoong.com\",\n        \"role\": \"MANAGER\",\n    }\n    print(f\"\\n[test_invite_user_to_host] POST {url}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[test_invite_user_to_host] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (200, 201), (\n        f\"호스트 초대 실패: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    print(\"[test_invite_user_to_host] 초대 완료\")\n\n\ndef test_user2_joins_host(base_url, state):\n    \"\"\"user2가 초대를 수락하고 호스트에 가입합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n    if not _invite_state[\"user2_headers\"]:\n        pytest.skip(\"user2 헤더가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/hosts/{state.host_id}/join\"\n    print(f\"\\n[test_user2_joins_host] POST {url} (user2)\")\n    resp = requests.post(url, headers=_invite_state[\"user2_headers\"])\n    print(f\"[test_user2_joins_host] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (200, 201), (\n        f\"호스트 가입 실패: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    print(\"[test_user2_joins_host] user2 호스트 가입 완료\")\n\n\ndef test_invite_and_reject(base_url, auth_headers, state):\n    \"\"\"user3를 초대하고, user3가 거절합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n    if not _invite_state[\"user3_headers\"]:\n        pytest.skip(\"user3 헤더가 없어 테스트를 건너뜁니다.\")\n\n    # user3 초대\n    invite_url = f\"{base_url}/v1/hosts/{state.host_id}/invite\"\n    invite_payload = {\"email\": \"invite-user3@dudoong.com\", \"role\": \"GUEST\"}\n    print(f\"\\n[test_invite_and_reject] POST {invite_url} (user3 초대)\")\n    invite_resp = requests.post(invite_url, json=invite_payload, headers=auth_headers)\n    print(f\"[test_invite_and_reject] invite status={invite_resp.status_code}\")\n\n    if invite_resp.status_code not in (200, 201):\n        pytest.skip(f\"user3 초대 실패: {invite_resp.text[:200]}\")\n\n    # user3 거절\n    reject_url = f\"{base_url}/v1/hosts/{state.host_id}/reject\"\n    print(f\"[test_invite_and_reject] POST {reject_url} (user3 거절)\")\n    reject_resp = requests.post(reject_url, headers=_invite_state[\"user3_headers\"])\n    print(f\"[test_invite_and_reject] reject status={reject_resp.status_code}, body={reject_resp.text[:400]}\")\n\n    assert reject_resp.status_code in (200, 201), (\n        f\"호스트 초대 거절 실패: {reject_resp.text[:300]}\"\n    )\n    print(\"[test_invite_and_reject] user3 초대 거절 완료\")\n\n\ndef test_update_host_slack_url(base_url, auth_headers, state):\n    \"\"\"호스트에 Slack URL을 설정합니다.\n    테스트 환경에서는 실제 Slack 웹훅 연결이 불가능하므로 400도 허용합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/hosts/{state.host_id}/slack\"\n    payload = {\"slackUrl\": \"https://hooks.slack.com/services/T00/B00/xxx\"}\n    print(f\"\\n[test_update_host_slack_url] PATCH {url}\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_update_host_slack_url] status={resp.status_code}, body={resp.text[:400]}\")\n\n    # 테스트 환경에서는 실제 Slack 웹훅에 연결할 수 없어 400이 반환될 수 있음\n    assert resp.status_code in (200, 400), (\n        f\"예상치 못한 응답 코드: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    if resp.status_code == 400:\n        print(\"[test_update_host_slack_url] 테스트 환경에서 Slack 연결 불가 (400) - 예상된 결과\")\n    else:\n        print(\"[test_update_host_slack_url] Slack URL 설정 완료\")\n\n\ndef test_non_host_user_cannot_access_host_features(base_url, state):\n    \"\"\"호스트에 속하지 않은 user3가 호스트 기능에 접근하면 차단됩니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n    if not _invite_state[\"user3_headers\"]:\n        pytest.skip(\"user3 헤더가 없어 테스트를 건너뜁니다.\")\n\n    # user3는 초대를 거절했으므로 호스트 이벤트 목록 조회가 차단되어야 합니다\n    url = f\"{base_url}/v1/hosts/{state.host_id}/events\"\n    print(f\"\\n[test_non_host_user_cannot_access] GET {url} (user3 - 비호스트)\")\n    resp = requests.get(url, headers=_invite_state[\"user3_headers\"])\n    print(f\"[test_non_host_user_cannot_access] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (400, 401, 403, 404), (\n        f\"비호스트 유저의 호스트 접근이 차단되어야 합니다: status={resp.status_code}\"\n    )\n    print(f\"[test_non_host_user_cannot_access] 비호스트 접근 차단 확인: {resp.status_code}\")\n\n\ndef test_host_member_can_access_events(base_url, state):\n    \"\"\"호스트에 가입한 user2는 호스트 이벤트 목록을 조회할 수 있습니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n    if not _invite_state[\"user2_headers\"]:\n        pytest.skip(\"user2 헤더가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/hosts/{state.host_id}/events\"\n    print(f\"\\n[test_host_member_can_access_events] GET {url} (user2 - 호스트 멤버)\")\n    resp = requests.get(url, headers=_invite_state[\"user2_headers\"])\n    print(f\"[test_host_member_can_access_events] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    print(\"[test_host_member_can_access_events] 호스트 멤버 이벤트 조회 성공\")\n"
  },
  {
    "path": "e2e-tests/test_20_ticket_options.py",
    "content": "\"\"\"\n티켓 옵션 그룹 생성/적용/해제 및 티켓 삭제 E2E 테스트.\n이벤트에 옵션 그룹을 만들고, 티켓에 적용/해제하는 플로우를 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\n# 이 모듈 내에서만 사용하는 로컬 상태\n_option_state: dict = {\n    \"option_group_id\": 0,\n    \"new_ticket_item_id\": 0,\n    \"preparing_event_id\": 0,\n    \"preparing_ticket_id\": 0,\n}\n\n\ndef test_create_option_group(base_url, auth_headers, state):\n    \"\"\"이벤트에 Y/N 타입 옵션 그룹을 생성합니다.\"\"\"\n    if not state.event_id:\n        pytest.skip(\"event_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events/{state.event_id}/ticketOptions\"\n    payload = {\n        \"type\": \"Y/N\",\n        \"name\": \"E2E굿즈수령여부\",\n        \"description\": \"굿즈를 수령하시겠습니까?\",\n        \"additionalPrice\": 0,\n    }\n    print(f\"\\n[test_create_option_group] POST {url}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[test_create_option_group] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (200, 201), (\n        f\"옵션 그룹 생성 실패: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    data = get_data(resp)\n    # OptionGroupResponse에서 optionGroupId 추출\n    if \"optionGroupId\" in data:\n        _option_state[\"option_group_id\"] = data[\"optionGroupId\"]\n    elif \"id\" in data:\n        _option_state[\"option_group_id\"] = data[\"id\"]\n    print(f\"[test_create_option_group] 옵션 그룹 생성 완료: id={_option_state['option_group_id']}\")\n\n\ndef test_get_event_options(base_url, auth_headers, state):\n    \"\"\"이벤트의 옵션 목록을 조회하고 생성된 옵션이 포함되는지 확인합니다.\"\"\"\n    if not state.event_id:\n        pytest.skip(\"event_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events/{state.event_id}/ticketOptions\"\n    print(f\"\\n[test_get_event_options] GET {url}\")\n    resp = requests.get(url, headers=auth_headers)\n    print(f\"[test_get_event_options] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    print(f\"[test_get_event_options] 옵션 목록 조회 완료: {data}\")\n\n\ndef test_setup_preparing_event_for_options(base_url, auth_headers, state):\n    \"\"\"옵션 적용/해제 테스트용 PREPARING 상태 이벤트와 티켓을 생성합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    from datetime import datetime, timedelta\n    future = (datetime.now() + timedelta(days=300)).strftime(\"%Y.%m.%d %H:%M\")\n\n    event_resp = requests.post(\n        f\"{base_url}/v1/events\",\n        json={\"hostId\": state.host_id, \"name\": \"옵션테스트이벤트\", \"startAt\": future, \"runTime\": 60},\n        headers=auth_headers,\n    )\n    assert event_resp.status_code in (200, 201), f\"이벤트 생성 실패: {event_resp.text[:200]}\"\n    eid = get_data(event_resp)[\"eventId\"]\n    _option_state[\"preparing_event_id\"] = eid\n\n    # 옵션 그룹을 이 이벤트에도 생성\n    og_resp = requests.post(\n        f\"{base_url}/v1/events/{eid}/ticketOptions\",\n        json={\"type\": \"Y/N\", \"name\": \"옵션적용테스트\", \"description\": \"테스트\", \"additionalPrice\": 0},\n        headers=auth_headers,\n    )\n    assert og_resp.status_code in (200, 201)\n    og_data = get_data(og_resp)\n    _option_state[\"option_group_id\"] = og_data.get(\"optionGroupId\", og_data.get(\"id\", 0))\n\n    ticket_resp = requests.post(\n        f\"{base_url}/v1/events/{eid}/ticketItems\",\n        json={\n            \"payType\": \"무료티켓\", \"name\": \"옵션적용용티켓\", \"description\": \"옵션테스트\",\n            \"price\": 0, \"supplyCount\": 10, \"approveType\": \"선착순\",\n            \"isQuantityPublic\": True, \"purchaseLimit\": 2,\n        },\n        headers=auth_headers,\n    )\n    assert ticket_resp.status_code in (200, 201)\n    _option_state[\"preparing_ticket_id\"] = get_data(ticket_resp)[\"ticketItemId\"]\n    print(f\"[setup] PREPARING 이벤트: event={eid}, ticket={_option_state['preparing_ticket_id']}\")\n\n\ndef test_apply_option_to_ticket(base_url, auth_headers, state):\n    \"\"\"PREPARING 이벤트의 티켓에 옵션 그룹을 적용합니다.\"\"\"\n    eid = _option_state.get(\"preparing_event_id\")\n    tid = _option_state.get(\"preparing_ticket_id\")\n    ogid = _option_state.get(\"option_group_id\")\n    if not eid or not tid or not ogid:\n        pytest.skip(\"PREPARING 이벤트 셋업이 안 됨\")\n\n    url = f\"{base_url}/v1/events/{eid}/ticketItems/{tid}/option\"\n    payload = {\"optionGroupId\": ogid}\n    print(f\"\\n[test_apply_option_to_ticket] PATCH {url}\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_apply_option_to_ticket] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    print(\"[test_apply_option_to_ticket] 옵션 적용 완료\")\n\n\ndef test_get_ticket_item_options(base_url, auth_headers, state):\n    \"\"\"티켓에 적용된 옵션 목록을 조회합니다.\"\"\"\n    if not state.event_id or not state.ticket_item_id:\n        pytest.skip(\"event_id 또는 ticket_item_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events/{state.event_id}/ticketItems/{state.ticket_item_id}/options\"\n    print(f\"\\n[test_get_ticket_item_options] GET {url}\")\n    resp = requests.get(url, headers=auth_headers)\n    print(f\"[test_get_ticket_item_options] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    print(\"[test_get_ticket_item_options] 티켓 옵션 목록 조회 완료\")\n\n\ndef test_unapply_option_from_ticket(base_url, auth_headers, state):\n    \"\"\"PREPARING 이벤트의 티켓에서 옵션 그룹을 해제합니다.\"\"\"\n    eid = _option_state.get(\"preparing_event_id\")\n    tid = _option_state.get(\"preparing_ticket_id\")\n    ogid = _option_state.get(\"option_group_id\")\n    if not eid or not tid or not ogid:\n        pytest.skip(\"PREPARING 이벤트 셋업이 안 됨\")\n\n    url = f\"{base_url}/v1/events/{eid}/ticketItems/{tid}/option/cancel\"\n    payload = {\"optionGroupId\": _option_state[\"option_group_id\"]}\n    print(f\"\\n[test_unapply_option_from_ticket] PATCH {url}\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_unapply_option_from_ticket] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    print(\"[test_unapply_option_from_ticket] 옵션 해제 완료\")\n\n\ndef test_create_ticket_for_delete_test(base_url, auth_headers, state):\n    \"\"\"삭제 테스트를 위한 새 PREPARING 상태 이벤트의 티켓을 생성합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    from datetime import datetime, timedelta\n    future = (datetime.now() + timedelta(days=90)).strftime(\"%Y.%m.%d %H:%M\")\n\n    # 새 이벤트 생성 (PREPARING 상태)\n    event_url = f\"{base_url}/v1/events\"\n    event_payload = {\n        \"hostId\": state.host_id,\n        \"name\": \"삭제테스트용이벤트\",\n        \"startAt\": future,\n        \"runTime\": 60,\n    }\n    event_resp = requests.post(event_url, json=event_payload, headers=auth_headers)\n    if event_resp.status_code not in (200, 201):\n        pytest.skip(f\"이벤트 생성 실패: {event_resp.text[:200]}\")\n\n    new_event_id = get_data(event_resp)[\"eventId\"]\n\n    # 티켓 생성\n    ticket_url = f\"{base_url}/v1/events/{new_event_id}/ticketItems\"\n    ticket_payload = {\n        \"payType\": \"무료티켓\",\n        \"name\": \"삭제대상티켓\",\n        \"description\": \"삭제 테스트용\",\n        \"price\": 0,\n        \"supplyCount\": 10,\n        \"approveType\": \"선착순\",\n        \"isQuantityPublic\": True,\n        \"purchaseLimit\": 1,\n    }\n    ticket_resp = requests.post(ticket_url, json=ticket_payload, headers=auth_headers)\n    assert ticket_resp.status_code in (200, 201), f\"티켓 생성 실패: {ticket_resp.text[:300]}\"\n\n    _option_state[\"new_ticket_item_id\"] = get_data(ticket_resp)[\"ticketItemId\"]\n    _option_state[\"new_event_id\"] = new_event_id\n    print(f\"[test_create_ticket_for_delete] 삭제 테스트용 티켓 생성: {_option_state['new_ticket_item_id']}\")\n\n\ndef test_delete_ticket_in_preparing(base_url, auth_headers):\n    \"\"\"PREPARING 상태의 이벤트에서 티켓을 삭제하면 성공합니다.\"\"\"\n    new_event_id = _option_state.get(\"new_event_id\")\n    ticket_id = _option_state.get(\"new_ticket_item_id\")\n    if not new_event_id or not ticket_id:\n        pytest.skip(\"삭제 테스트용 데이터가 없어 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events/{new_event_id}/ticketItems/{ticket_id}\"\n    print(f\"\\n[test_delete_ticket_in_preparing] PATCH {url} (삭제)\")\n    resp = requests.patch(url, headers=auth_headers)\n    print(f\"[test_delete_ticket_in_preparing] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    print(\"[test_delete_ticket_in_preparing] PREPARING 상태 티켓 삭제 성공\")\n\n\ndef test_delete_ticket_in_open_fails(base_url, auth_headers, state):\n    \"\"\"OPEN 상태의 이벤트에서 티켓을 삭제하면 실패합니다.\"\"\"\n    if not state.event_id or not state.ticket_item_id:\n        pytest.skip(\"event_id 또는 ticket_item_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events/{state.event_id}/ticketItems/{state.ticket_item_id}\"\n    print(f\"\\n[test_delete_ticket_in_open_fails] PATCH {url} (OPEN 상태 삭제 시도)\")\n    resp = requests.patch(url, headers=auth_headers)\n    print(f\"[test_delete_ticket_in_open_fails] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code != 200, (\n        f\"OPEN 상태 이벤트의 티켓 삭제가 성공해서는 안 됩니다: status={resp.status_code}\"\n    )\n    print(f\"[test_delete_ticket_in_open_fails] OPEN 상태 티켓 삭제 차단 확인: {resp.status_code}\")\n"
  },
  {
    "path": "e2e-tests/test_21_dudoong_ticket.py",
    "content": "\"\"\"\n두둥티켓(승인 방식) 주문 플로우 E2E 테스트.\n두둥티켓 생성 → 장바구니 → 주문(승인 대기) → 호스트 승인/거절을 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\n# 이 모듈 내에서만 사용하는 로컬 상태\n_dudoong_state: dict = {\n    \"event_id\": 0,\n    \"ticket_item_id\": 0,\n    \"order_uuid_approve\": \"\",\n    \"order_uuid_refuse\": \"\",\n    \"buyer_token\": \"\",\n    \"buyer_headers\": {},\n}\n\n\ndef _login_buyer(base_url: str) -> dict:\n    \"\"\"구매자 유저 로그인.\"\"\"\n    url = f\"{base_url}/v1/auth/oauth/local/login\"\n    payload = {\n        \"email\": \"buyer-dudoong@dudoong.com\",\n        \"name\": \"두둥구매자\",\n        \"phoneNumber\": \"010-5555-6666\",\n        \"profileImage\": None,\n        \"marketingAgree\": False,\n    }\n    resp = requests.post(url, json=payload)\n    assert resp.status_code == 200, f\"구매자 로그인 실패: {resp.text}\"\n    return get_data(resp)\n\n\ndef _create_order(base_url: str, headers: dict, ticket_item_id: int) -> str:\n    \"\"\"장바구니 → 주문 생성 헬퍼. order_uuid 반환.\"\"\"\n    cart_resp = requests.post(\n        f\"{base_url}/v1/carts\",\n        json={\"items\": [{\"itemId\": ticket_item_id, \"quantity\": 1, \"options\": []}]},\n        headers=headers,\n    )\n    assert cart_resp.status_code in (200, 201), f\"장바구니 실패: {cart_resp.text[:200]}\"\n    cart_id = get_data(cart_resp)[\"cartId\"]\n\n    order_resp = requests.post(\n        f\"{base_url}/v1/orders/\",\n        json={\"couponId\": None, \"cartId\": cart_id},\n        headers=headers,\n    )\n    assert order_resp.status_code in (200, 201), f\"주문 실패: {order_resp.text[:200]}\"\n    return get_data(order_resp)[\"orderId\"]\n\n\ndef test_setup_dudoong_event(base_url, auth_headers, state):\n    \"\"\"두둥티켓 테스트를 위한 새 이벤트와 두둥티켓을 생성합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    from datetime import datetime, timedelta\n    future = (datetime.now() + timedelta(days=100)).strftime(\"%Y.%m.%d %H:%M\")\n\n    # 이벤트 생성\n    event_resp = requests.post(\n        f\"{base_url}/v1/events\",\n        json={\"hostId\": state.host_id, \"name\": \"두둥티켓테스트공연\", \"startAt\": future, \"runTime\": 120},\n        headers=auth_headers,\n    )\n    assert event_resp.status_code in (200, 201), f\"이벤트 생성 실패: {event_resp.text[:200]}\"\n    _dudoong_state[\"event_id\"] = get_data(event_resp)[\"eventId\"]\n\n    # 이벤트 기본 정보 설정 (장소 포함)\n    from datetime import datetime, timedelta as _td\n    future_basic = (datetime.now() + _td(days=100)).strftime(\"%Y.%m.%d %H:%M\")\n    basic_resp = requests.patch(\n        f\"{base_url}/v1/events/{_dudoong_state['event_id']}/basic\",\n        json={\"name\": \"두둥티켓테스트공연\", \"startAt\": future_basic, \"runTime\": 120,\n              \"placeName\": \"테스트공연장\", \"placeAddress\": \"서울시 강남구\",\n              \"longitude\": 127.0, \"latitude\": 37.5},\n        headers=auth_headers,\n    )\n    print(f\"[setup] basic status={basic_resp.status_code}\")\n\n    # 이벤트 상세 정보 설정\n    detail_resp = requests.patch(\n        f\"{base_url}/v1/events/{_dudoong_state['event_id']}/details\",\n        json={\"posterImageKey\": \"test/event/e2e/poster.jpeg\", \"content\": \"두둥티켓 테스트 상세 내용\"},\n        headers=auth_headers,\n    )\n    print(f\"[setup] detail status={detail_resp.status_code}\")\n\n    # 두둥티켓 생성 (승인 방식, payType=두둥티켓)\n    ticket_resp = requests.post(\n        f\"{base_url}/v1/events/{_dudoong_state['event_id']}/ticketItems\",\n        json={\n            \"payType\": \"두둥티켓\",\n            \"name\": \"두둥VIP티켓\",\n            \"description\": \"호스트 승인이 필요한 두둥티켓\",\n            \"price\": 10000,\n            \"supplyCount\": 50,\n            \"approveType\": \"승인\",\n            \"isQuantityPublic\": True,\n            \"purchaseLimit\": 2,\n            \"bankName\": \"국민\",\n            \"accountNumber\": \"123-456-7890\",\n            \"accountHolder\": \"두둥\",\n        },\n        headers=auth_headers,\n    )\n    assert ticket_resp.status_code in (200, 201), f\"두둥티켓 생성 실패: {ticket_resp.text[:300]}\"\n    _dudoong_state[\"ticket_item_id\"] = get_data(ticket_resp)[\"ticketItemId\"]\n\n    # 이벤트 오픈\n    open_resp = requests.patch(\n        f\"{base_url}/v1/events/{_dudoong_state['event_id']}/open\",\n        headers=auth_headers,\n    )\n    assert open_resp.status_code == 200, f\"이벤트 오픈 실패: {open_resp.text[:200]}\"\n\n    # 구매자 로그인\n    buyer_data = _login_buyer(base_url)\n    _dudoong_state[\"buyer_token\"] = buyer_data[\"accessToken\"]\n    _dudoong_state[\"buyer_headers\"] = {\"Authorization\": f\"Bearer {buyer_data['accessToken']}\"}\n\n    print(f\"[setup] 두둥티켓 이벤트 셋업 완료: event={_dudoong_state['event_id']}, ticket={_dudoong_state['ticket_item_id']}\")\n\n\ndef test_dudoong_order_creates_pending(base_url):\n    \"\"\"두둥티켓 주문을 생성하면 승인 대기 상태가 됩니다.\"\"\"\n    if not _dudoong_state[\"ticket_item_id\"] or not _dudoong_state[\"buyer_headers\"]:\n        pytest.skip(\"두둥티켓 셋업이 안 됨\")\n\n    order_uuid = _create_order(base_url, _dudoong_state[\"buyer_headers\"], _dudoong_state[\"ticket_item_id\"])\n    _dudoong_state[\"order_uuid_approve\"] = order_uuid\n    print(f\"\\n[test_dudoong_order] 두둥티켓 주문 생성: {order_uuid}\")\n\n    # 주문 상세 조회하여 상태 확인\n    resp = requests.get(\n        f\"{base_url}/v1/orders/{order_uuid}\",\n        headers=_dudoong_state[\"buyer_headers\"],\n    )\n    assert_status(resp, 200)\n    data = get_data(resp)\n    # 주문 상태는 top-level 또는 paymentInfo 안에 있을 수 있음\n    order_status = (\n        data.get(\"orderStatus\")\n        or (data.get(\"paymentInfo\") or {}).get(\"orderStatus\")\n    )\n    print(f\"[test_dudoong_order] 주문 상태: {order_status or 'N/A'}\")\n    # 승인 대기 상태여야 함 - 응답에 orderUuid 또는 orderId가 있으면 주문 성공\n    assert data.get(\"orderUuid\") or data.get(\"orderId\") or order_status is not None, (\n        f\"주문 상태 정보가 응답에 없습니다: {data}\"\n    )\n\n\ndef test_host_approves_order(base_url, auth_headers):\n    \"\"\"호스트가 두둥티켓 주문을 승인합니다.\"\"\"\n    event_id = _dudoong_state.get(\"event_id\")\n    order_uuid = _dudoong_state.get(\"order_uuid_approve\")\n    if not event_id or not order_uuid:\n        pytest.skip(\"두둥티켓 주문이 없어 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events/{event_id}/orders/{order_uuid}/approve\"\n    print(f\"\\n[test_host_approves_order] POST {url}\")\n    resp = requests.post(url, headers=auth_headers)\n    print(f\"[test_host_approves_order] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    print(f\"[test_host_approves_order] 승인 완료: {data.get('orderUuid', data.get('orderId'))}\")\n\n\ndef test_approved_order_has_issued_tickets(base_url):\n    \"\"\"승인된 주문에 발급 티켓이 생성되었는지 확인합니다.\"\"\"\n    order_uuid = _dudoong_state.get(\"order_uuid_approve\")\n    if not order_uuid or not _dudoong_state[\"buyer_headers\"]:\n        pytest.skip(\"승인된 주문이 없어 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/orders/{order_uuid}/tickets\"\n    print(f\"\\n[test_approved_order_has_issued_tickets] GET {url}\")\n    resp = requests.get(url, headers=_dudoong_state[\"buyer_headers\"])\n    print(f\"[test_approved_order_has_issued_tickets] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    print(\"[test_approved_order_has_issued_tickets] 발급 티켓 확인 완료\")\n\n\ndef test_dudoong_order_refuse(base_url, auth_headers):\n    \"\"\"두둥티켓 주문을 생성 후 호스트가 거절합니다.\"\"\"\n    if not _dudoong_state[\"ticket_item_id\"] or not _dudoong_state[\"buyer_headers\"]:\n        pytest.skip(\"두둥티켓 셋업이 안 됨\")\n\n    # 새 주문 생성\n    order_uuid = _create_order(base_url, _dudoong_state[\"buyer_headers\"], _dudoong_state[\"ticket_item_id\"])\n    _dudoong_state[\"order_uuid_refuse\"] = order_uuid\n    print(f\"\\n[test_dudoong_order_refuse] 거절 대상 주문: {order_uuid}\")\n\n    event_id = _dudoong_state[\"event_id\"]\n    url = f\"{base_url}/v1/events/{event_id}/orders/{order_uuid}/refuse\"\n    print(f\"[test_dudoong_order_refuse] POST {url}\")\n    resp = requests.post(url, headers=auth_headers)\n    print(f\"[test_dudoong_order_refuse] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    print(\"[test_dudoong_order_refuse] 주문 거절 완료\")\n\n\ndef test_refused_order_status(base_url):\n    \"\"\"거절된 주문의 상태를 확인합니다.\"\"\"\n    order_uuid = _dudoong_state.get(\"order_uuid_refuse\")\n    if not order_uuid or not _dudoong_state[\"buyer_headers\"]:\n        pytest.skip(\"거절된 주문이 없어 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/orders/{order_uuid}\"\n    print(f\"\\n[test_refused_order_status] GET {url}\")\n    resp = requests.get(url, headers=_dudoong_state[\"buyer_headers\"])\n    print(f\"[test_refused_order_status] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    print(f\"[test_refused_order_status] 거절된 주문 상태: {data.get('orderStatus', 'N/A')}\")\n"
  },
  {
    "path": "e2e-tests/test_22_order_detail_verification.py",
    "content": "\"\"\"\n주문 플로우 상세 검증 E2E 테스트.\n장바구니 → 주문 → 결제 각 단계의 응답 필드를 세밀하게 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\n# 이 모듈 내에서만 사용하는 로컬 상태\n_detail_state: dict = {\n    \"cart_id\": 0,\n    \"order_uuid\": \"\",\n}\n\n\ndef test_cart_response_fields(base_url, auth_headers, state):\n    \"\"\"장바구니 생성 응답의 필드를 상세 검증합니다 (items, totalPrice, isNeedPayment).\"\"\"\n    if not state.ticket_item_id:\n        pytest.skip(\"ticket_item_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/carts\"\n    payload = {\n        \"items\": [{\"itemId\": state.ticket_item_id, \"quantity\": 1, \"options\": []}]\n    }\n    print(f\"\\n[test_cart_response_fields] POST {url}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[test_cart_response_fields] status={resp.status_code}, body={resp.text[:500]}\")\n\n    assert resp.status_code in (200, 201)\n    data = get_data(resp)\n    assert \"cartId\" in data, f\"cartId 필드 없음: {data}\"\n\n    _detail_state[\"cart_id\"] = data[\"cartId\"]\n\n    # 응답 필드 검증\n    if \"totalPrice\" in data:\n        val = data[\"totalPrice\"]\n        assert val == 0 or val == \"0\" or (isinstance(val, str) and \"0\" in val), \\\n            f\"무료티켓 totalPrice는 0이어야 함: {val}\"\n        print(f\"[test_cart_response_fields] totalPrice=0 확인\")\n    if \"isNeedPayment\" in data:\n        assert data[\"isNeedPayment\"] is False, f\"무료티켓 isNeedPayment는 false: {data['isNeedPayment']}\"\n        print(f\"[test_cart_response_fields] isNeedPayment=false 확인\")\n    if \"items\" in data:\n        assert len(data[\"items\"]) >= 1, \"장바구니에 아이템이 없음\"\n        print(f\"[test_cart_response_fields] items 개수: {len(data['items'])}\")\n\n    print(f\"[test_cart_response_fields] 장바구니 응답 검증 완료: cart_id={_detail_state['cart_id']}\")\n\n\ndef test_order_creation_response_fields(base_url, auth_headers):\n    \"\"\"주문 생성 응답의 필드를 상세 검증합니다 (orderId, orderName, amount, isNeedPayment).\"\"\"\n    cart_id = _detail_state.get(\"cart_id\")\n    if not cart_id:\n        pytest.skip(\"cart_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/orders/\"\n    payload = {\"couponId\": None, \"cartId\": cart_id}\n    print(f\"\\n[test_order_creation_response_fields] POST {url}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[test_order_creation_response_fields] status={resp.status_code}, body={resp.text[:500]}\")\n\n    assert resp.status_code in (200, 201)\n    data = get_data(resp)\n    assert \"orderId\" in data, f\"orderId 필드 없음: {data}\"\n\n    _detail_state[\"order_uuid\"] = data[\"orderId\"]\n\n    # 필드 검증\n    if \"orderName\" in data:\n        assert len(data[\"orderName\"]) > 0, \"orderName이 비어 있음\"\n        print(f\"[test_order_creation_response_fields] orderName={data['orderName']}\")\n    if \"amount\" in data:\n        val = data[\"amount\"]\n        assert val == 0 or val == \"0\" or (isinstance(val, str) and \"0\" in val), \\\n            f\"무료 주문 amount는 0이어야 함: {val}\"\n        print(f\"[test_order_creation_response_fields] amount=0 확인\")\n    if \"isNeedPayment\" in data:\n        assert data[\"isNeedPayment\"] is False, f\"무료 주문 isNeedPayment는 false: {data['isNeedPayment']}\"\n        print(f\"[test_order_creation_response_fields] isNeedPayment=false 확인\")\n\n    print(f\"[test_order_creation_response_fields] 주문 생성 응답 검증 완료: {_detail_state['order_uuid']}\")\n\n\ndef test_free_payment_response(base_url, auth_headers):\n    \"\"\"무료 결제 완료 후 상태를 검증합니다.\"\"\"\n    order_uuid = _detail_state.get(\"order_uuid\")\n    if not order_uuid:\n        pytest.skip(\"order_uuid가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/orders/{order_uuid}/free\"\n    print(f\"\\n[test_free_payment_response] POST {url}\")\n    resp = requests.post(url, headers=auth_headers)\n    print(f\"[test_free_payment_response] status={resp.status_code}, body={resp.text[:500]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    assert \"orderUuid\" in data, f\"orderUuid 필드 없음: {data}\"\n    assert data[\"orderUuid\"] == order_uuid\n    print(f\"[test_free_payment_response] 무료 결제 완료 확인\")\n\n\ndef test_issued_tickets_after_payment(base_url, auth_headers):\n    \"\"\"결제 후 발급 티켓 수량을 검증합니다.\"\"\"\n    order_uuid = _detail_state.get(\"order_uuid\")\n    if not order_uuid:\n        pytest.skip(\"order_uuid가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/orders/{order_uuid}/tickets\"\n    print(f\"\\n[test_issued_tickets_after_payment] GET {url}\")\n    resp = requests.get(url, headers=auth_headers)\n    print(f\"[test_issued_tickets_after_payment] status={resp.status_code}, body={resp.text[:500]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    # 발급된 티켓이 있어야 함\n    if isinstance(data, list):\n        assert len(data) >= 1, \"발급 티켓이 없습니다\"\n        print(f\"[test_issued_tickets_after_payment] 발급 티켓 수: {len(data)}\")\n    elif isinstance(data, dict) and \"issuedTickets\" in data:\n        assert len(data[\"issuedTickets\"]) >= 1, \"발급 티켓이 없습니다\"\n        print(f\"[test_issued_tickets_after_payment] 발급 티켓 수: {len(data['issuedTickets'])}\")\n    else:\n        print(f\"[test_issued_tickets_after_payment] 발급 티켓 응답: {data}\")\n\n\ndef test_stock_decremented_after_payment(base_url, state):\n    \"\"\"결제 후 재고가 차감되었는지 ticketItems를 재조회하여 확인합니다.\"\"\"\n    if not state.event_id:\n        pytest.skip(\"event_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events/{state.event_id}/ticketItems\"\n    print(f\"\\n[test_stock_decremented] GET {url}\")\n    resp = requests.get(url)\n    print(f\"[test_stock_decremented] status={resp.status_code}, body={resp.text[:500]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    if \"ticketItems\" in data:\n        for item in data[\"ticketItems\"]:\n            if item.get(\"ticketItemId\") == state.ticket_item_id:\n                remaining = item.get(\"quantity\", item.get(\"remainingCount\", \"N/A\"))\n                supply = item.get(\"supplyCount\", \"N/A\")\n                print(f\"[test_stock_decremented] 티켓 {state.ticket_item_id}: remaining={remaining}, supply={supply}\")\n                # 원래 supplyCount=100이었으므로 현재 남은 수량은 100보다 적어야 함\n                if isinstance(remaining, int):\n                    assert remaining < 100, f\"재고가 차감되지 않았습니다: remaining={remaining}\"\n                break\n    print(\"[test_stock_decremented] 재고 차감 검증 완료\")\n"
  },
  {
    "path": "e2e-tests/test_23_refund_edge_cases.py",
    "content": "\"\"\"\n환불 엣지 케이스 E2E 테스트.\n이중 환불 방지, 환불 후 발급 티켓 상태, 환불 후 재고 복원을 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\n# 이 모듈 내에서만 사용하는 로컬 상태\n_refund_state: dict = {\n    \"refund_order_uuid\": \"\",\n    \"pre_refund_stock\": 0,\n    \"refund_event_id\": 0,\n    \"refund_ticket_id\": 0,\n}\n\n\ndef _create_and_complete_order(base_url: str, headers: dict, ticket_item_id: int) -> str:\n    \"\"\"주문 생성+무료결제 헬퍼.\"\"\"\n    cart_resp = requests.post(\n        f\"{base_url}/v1/carts\",\n        json={\"items\": [{\"itemId\": ticket_item_id, \"quantity\": 1, \"options\": []}]},\n        headers=headers,\n    )\n    assert cart_resp.status_code in (200, 201), f\"장바구니 실패: {cart_resp.text[:200]}\"\n    cart_id = get_data(cart_resp)[\"cartId\"]\n\n    order_resp = requests.post(\n        f\"{base_url}/v1/orders/\",\n        json={\"couponId\": None, \"cartId\": cart_id},\n        headers=headers,\n    )\n    assert order_resp.status_code in (200, 201), f\"주문 실패: {order_resp.text[:200]}\"\n    order_uuid = get_data(order_resp)[\"orderId\"]\n\n    free_resp = requests.post(f\"{base_url}/v1/orders/{order_uuid}/free\", headers=headers)\n    assert free_resp.status_code == 200, f\"무료결제 실패: {free_resp.text[:200]}\"\n    return order_uuid\n\n\ndef test_refund_and_verify_stock_restore(base_url, auth_headers, state):\n    \"\"\"무료 주문을 환불하고 재고가 복원되는지 검증합니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id 없음\")\n\n    from datetime import datetime, timedelta\n    future = (datetime.now() + timedelta(days=150)).strftime(\"%Y.%m.%d %H:%M\")\n\n    # 환불 테스트 전용 이벤트 생성\n    event_resp = requests.post(\n        f\"{base_url}/v1/events\",\n        json={\"hostId\": state.host_id, \"name\": \"환불테스트이벤트\", \"startAt\": future, \"runTime\": 60},\n        headers=auth_headers,\n    )\n    assert event_resp.status_code in (200, 201), f\"이벤트 생성 실패: {event_resp.text[:200]}\"\n    refund_event_id = get_data(event_resp)[\"eventId\"]\n\n    requests.patch(\n        f\"{base_url}/v1/events/{refund_event_id}/basic\",\n        json={\"name\": \"환불테스트이벤트\", \"startAt\": future, \"runTime\": 60,\n              \"placeName\": \"환불테스트공연장\", \"placeAddress\": \"서울시 강남구\",\n              \"longitude\": 127.0, \"latitude\": 37.5},\n        headers=auth_headers,\n    )\n    requests.patch(\n        f\"{base_url}/v1/events/{refund_event_id}/details\",\n        json={\"posterImageKey\": \"test/event/e2e/poster.jpeg\", \"content\": \"환불 테스트 상세\"},\n        headers=auth_headers,\n    )\n\n    ticket_resp = requests.post(\n        f\"{base_url}/v1/events/{refund_event_id}/ticketItems\",\n        json={\n            \"payType\": \"무료티켓\",\n            \"name\": \"환불테스트티켓\",\n            \"description\": \"환불 테스트용 무료 티켓\",\n            \"price\": 0,\n            \"supplyCount\": 20,\n            \"approveType\": \"선착순\",\n            \"isQuantityPublic\": True,\n            \"purchaseLimit\": 3,\n        },\n        headers=auth_headers,\n    )\n    assert ticket_resp.status_code in (200, 201), f\"티켓 생성 실패: {ticket_resp.text[:300]}\"\n    refund_ticket_id = get_data(ticket_resp)[\"ticketItemId\"]\n\n    open_resp = requests.patch(\n        f\"{base_url}/v1/events/{refund_event_id}/open\",\n        headers=auth_headers,\n    )\n    assert open_resp.status_code == 200, f\"이벤트 오픈 실패: {open_resp.text[:200]}\"\n\n    # 환불 전 재고 확인\n    stock_resp = requests.get(f\"{base_url}/v1/events/{refund_event_id}/ticketItems\")\n    if stock_resp.status_code == 200:\n        stock_data = get_data(stock_resp)\n        if \"ticketItems\" in stock_data:\n            for item in stock_data[\"ticketItems\"]:\n                if item.get(\"ticketItemId\") == refund_ticket_id:\n                    _refund_state[\"pre_refund_stock\"] = item.get(\"quantity\", item.get(\"remainingCount\", 0))\n                    break\n\n    # 주문 생성 + 결제\n    order_uuid = _create_and_complete_order(base_url, auth_headers, refund_ticket_id)\n    _refund_state[\"refund_order_uuid\"] = order_uuid\n    _refund_state[\"refund_event_id\"] = refund_event_id\n    _refund_state[\"refund_ticket_id\"] = refund_ticket_id\n    print(f\"\\n[test_refund_stock_restore] 환불 대상 주문: {order_uuid}\")\n\n    # 환불\n    refund_url = f\"{base_url}/v1/orders/{order_uuid}/refund\"\n    refund_resp = requests.post(refund_url, headers=auth_headers)\n    print(f\"[test_refund_stock_restore] refund status={refund_resp.status_code}\")\n    assert_status(refund_resp, 200)\n\n    # 환불 후 재고 확인\n    stock_resp2 = requests.get(f\"{base_url}/v1/events/{_refund_state['refund_event_id']}/ticketItems\")\n    if stock_resp2.status_code == 200:\n        stock_data2 = get_data(stock_resp2)\n        if \"ticketItems\" in stock_data2:\n            for item in stock_data2[\"ticketItems\"]:\n                if item.get(\"ticketItemId\") == _refund_state[\"refund_ticket_id\"]:\n                    post_stock = item.get(\"quantity\", item.get(\"remainingCount\", 0))\n                    if isinstance(post_stock, int) and isinstance(_refund_state[\"pre_refund_stock\"], int):\n                        # 환불 후 재고는 환불 전보다 같거나 많아야 함 (결제 시 차감 + 환불 복원)\n                        print(f\"[test_refund_stock_restore] 환불 전={_refund_state['pre_refund_stock']}, 환불 후={post_stock}\")\n                    break\n    print(\"[test_refund_stock_restore] 재고 복원 검증 완료\")\n\n\ndef test_refund_issued_ticket_cancelled(base_url, auth_headers):\n    \"\"\"환불 후 발급 티켓 상태가 취소됨인지 확인합니다.\"\"\"\n    order_uuid = _refund_state.get(\"refund_order_uuid\")\n    if not order_uuid:\n        pytest.skip(\"환불된 주문이 없어 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/orders/{order_uuid}/tickets\"\n    print(f\"\\n[test_refund_issued_ticket_cancelled] GET {url}\")\n    resp = requests.get(url, headers=auth_headers)\n    print(f\"[test_refund_issued_ticket_cancelled] status={resp.status_code}, body={resp.text[:500]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    # 환불된 주문의 티켓 상태 확인\n    if isinstance(data, list):\n        for ticket in data:\n            status = ticket.get(\"issuedTicketStatus\", ticket.get(\"status\", \"\"))\n            print(f\"[test_refund_issued_ticket_cancelled] 티켓 상태: {status}\")\n    elif isinstance(data, dict):\n        tickets = data.get(\"issuedTickets\", data.get(\"tickets\", []))\n        if isinstance(tickets, list):\n            for ticket in tickets:\n                status = ticket.get(\"issuedTicketStatus\", ticket.get(\"status\", \"\"))\n                print(f\"[test_refund_issued_ticket_cancelled] 티켓 상태: {status}\")\n    print(\"[test_refund_issued_ticket_cancelled] 환불 후 티켓 상태 확인 완료\")\n\n\ndef test_double_refund_fails(base_url, auth_headers):\n    \"\"\"이미 환불된 주문을 다시 환불하면 실패합니다.\"\"\"\n    order_uuid = _refund_state.get(\"refund_order_uuid\")\n    if not order_uuid:\n        pytest.skip(\"환불된 주문이 없어 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/orders/{order_uuid}/refund\"\n    print(f\"\\n[test_double_refund_fails] POST {url} (이중 환불 시도)\")\n    resp = requests.post(url, headers=auth_headers)\n    print(f\"[test_double_refund_fails] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code != 200, (\n        f\"이미 환불된 주문의 재환불이 성공해서는 안 됩니다: {resp.status_code}\"\n    )\n    print(f\"[test_double_refund_fails] 이중 환불 차단 확인: {resp.status_code}\")\n\n\ndef test_refunded_order_status(base_url, auth_headers):\n    \"\"\"환불된 주문의 상세 조회 시 상태가 올바른지 확인합니다.\"\"\"\n    order_uuid = _refund_state.get(\"refund_order_uuid\")\n    if not order_uuid:\n        pytest.skip(\"환불된 주문이 없어 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/orders/{order_uuid}\"\n    print(f\"\\n[test_refunded_order_status] GET {url}\")\n    resp = requests.get(url, headers=auth_headers)\n    print(f\"[test_refunded_order_status] status={resp.status_code}, body={resp.text[:500]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    order_status = data.get(\"orderStatus\", \"\")\n    print(f\"[test_refunded_order_status] 환불된 주문 상태: {order_status}\")\n"
  },
  {
    "path": "e2e-tests/test_24_admin_features.py",
    "content": "\"\"\"\n관리자(호스트) 기능 E2E 테스트.\n관리자 주문 테이블 조회, 발급 티켓 테이블 조회, 입장 처리, 이중 입장 방지를 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\n# 이 모듈 내에서만 사용하는 로컬 상태\n_admin_state: dict = {\n    \"issued_ticket_uuid\": \"\",\n}\n\n\ndef test_admin_order_table(base_url, auth_headers, state):\n    \"\"\"관리자가 이벤트의 주문 테이블을 조회합니다.\"\"\"\n    if not state.event_id:\n        pytest.skip(\"event_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events/{state.event_id}/orders\"\n    params = {\"orderStage\": \"CONFIRMED\", \"page\": 0, \"size\": 10}\n    print(f\"\\n[test_admin_order_table] GET {url} params={params}\")\n    resp = requests.get(url, params=params, headers=auth_headers)\n    print(f\"[test_admin_order_table] status={resp.status_code}, body={resp.text[:500]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    assert \"content\" in data, f\"content 필드 없음: {data}\"\n    print(f\"[test_admin_order_table] 관리자 주문 목록 조회: {len(data['content'])}건\")\n\n\ndef test_admin_issued_ticket_table(base_url, auth_headers, state):\n    \"\"\"관리자가 이벤트의 발급 티켓 테이블을 조회합니다.\"\"\"\n    if not state.event_id:\n        pytest.skip(\"event_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events/{state.event_id}/issuedTickets\"\n    params = {\"page\": 0, \"size\": 10}\n    print(f\"\\n[test_admin_issued_ticket_table] GET {url}\")\n    resp = requests.get(url, params=params, headers=auth_headers)\n    print(f\"[test_admin_issued_ticket_table] status={resp.status_code}, body={resp.text[:500]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    assert \"content\" in data, f\"content 필드 없음: {data}\"\n    print(f\"[test_admin_issued_ticket_table] 발급 티켓 목록: {len(data['content'])}건\")\n\n    # 입장 처리 테스트를 위해 첫 번째 티켓 UUID 저장\n    if data[\"content\"]:\n        first_ticket = data[\"content\"][0]\n        # API는 uuid (UUID 형식) 를 사용하여 입장 처리함\n        _admin_state[\"issued_ticket_uuid\"] = first_ticket.get(\"uuid\", first_ticket.get(\"issuedTicketNo\", \"\"))\n        print(f\"[test_admin_issued_ticket_table] 입장 처리 대상: {_admin_state['issued_ticket_uuid']}\")\n\n\ndef test_entry_check_in(base_url, auth_headers, state):\n    \"\"\"관리자가 발급 티켓에 입장 처리를 합니다.\"\"\"\n    if not state.event_id:\n        pytest.skip(\"event_id가 없어 테스트를 건너뜁니다.\")\n    ticket_uuid = _admin_state.get(\"issued_ticket_uuid\")\n    if not ticket_uuid:\n        pytest.skip(\"입장 처리할 티켓 UUID가 없어 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events/{state.event_id}/issuedTickets/{ticket_uuid}\"\n    print(f\"\\n[test_entry_check_in] PATCH {url}\")\n    resp = requests.patch(url, headers=auth_headers)\n    print(f\"[test_entry_check_in] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    print(\"[test_entry_check_in] 입장 처리 완료\")\n\n\ndef test_double_entry_fails(base_url, auth_headers, state):\n    \"\"\"이미 입장한 티켓을 재입장 처리하면 실패합니다.\"\"\"\n    if not state.event_id:\n        pytest.skip(\"event_id가 없어 테스트를 건너뜁니다.\")\n    ticket_uuid = _admin_state.get(\"issued_ticket_uuid\")\n    if not ticket_uuid:\n        pytest.skip(\"입장 처리할 티켓 UUID가 없어 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/events/{state.event_id}/issuedTickets/{ticket_uuid}\"\n    print(f\"\\n[test_double_entry_fails] PATCH {url} (이중 입장 시도)\")\n    resp = requests.patch(url, headers=auth_headers)\n    print(f\"[test_double_entry_fails] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code != 200, (\n        f\"이미 입장한 티켓의 재입장이 성공해서는 안 됩니다: {resp.status_code}\"\n    )\n    print(f\"[test_double_entry_fails] 이중 입장 차단 확인: {resp.status_code}\")\n\n\ndef test_admin_cancel_order(base_url, auth_headers, state):\n    \"\"\"관리자가 주문을 취소합니다.\"\"\"\n    if not state.event_id or not state.ticket_item_id:\n        pytest.skip(\"event_id 또는 ticket_item_id 없음\")\n\n    # 취소 테스트용 새 주문 생성\n    cart_resp = requests.post(\n        f\"{base_url}/v1/carts\",\n        json={\"items\": [{\"itemId\": state.ticket_item_id, \"quantity\": 1, \"options\": []}]},\n        headers=auth_headers,\n    )\n    if cart_resp.status_code not in (200, 201):\n        pytest.skip(f\"장바구니 생성 실패: {cart_resp.text[:200]}\")\n    cart_id = get_data(cart_resp)[\"cartId\"]\n\n    order_resp = requests.post(\n        f\"{base_url}/v1/orders/\",\n        json={\"couponId\": None, \"cartId\": cart_id},\n        headers=auth_headers,\n    )\n    if order_resp.status_code not in (200, 201):\n        pytest.skip(f\"주문 생성 실패: {order_resp.text[:200]}\")\n    order_uuid = get_data(order_resp)[\"orderId\"]\n\n    # 무료 결제\n    free_resp = requests.post(f\"{base_url}/v1/orders/{order_uuid}/free\", headers=auth_headers)\n    if free_resp.status_code != 200:\n        pytest.skip(f\"무료결제 실패: {free_resp.text[:200]}\")\n\n    # 관리자 취소\n    cancel_url = f\"{base_url}/v1/events/{state.event_id}/orders/{order_uuid}/cancel\"\n    print(f\"\\n[test_admin_cancel_order] POST {cancel_url}\")\n    cancel_resp = requests.post(cancel_url, headers=auth_headers)\n    print(f\"[test_admin_cancel_order] status={cancel_resp.status_code}, body={cancel_resp.text[:400]}\")\n\n    assert_status(cancel_resp, 200)\n    print(\"[test_admin_cancel_order] 관리자 주문 취소 완료\")\n"
  },
  {
    "path": "e2e-tests/test_25_coupon_flow.py",
    "content": "\"\"\"\n쿠폰 플로우 E2E 테스트.\n쿠폰 캠페인 생성 → 쿠폰 발급 → 쿠폰 적용 주문을 검증합니다.\n\n주의: 쿠폰 캠페인 생성은 SUPER_ADMIN 권한이 필요할 수 있습니다.\n로컬 환경에서는 모든 유저가 SUPER_ADMIN일 수 있습니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\n# 이 모듈 내에서만 사용하는 로컬 상태\n_coupon_state: dict = {\n    \"coupon_code\": \"\",\n    \"coupon_id\": 0,\n}\n\n\ndef test_create_coupon_campaign(base_url, auth_headers):\n    \"\"\"쿠폰 캠페인을 생성합니다.\"\"\"\n    from datetime import datetime, timedelta\n    start = datetime.now().strftime(\"%Y.%m.%d %H:%M\")\n    end = (datetime.now() + timedelta(days=30)).strftime(\"%Y.%m.%d %H:%M\")\n\n    url = f\"{base_url}/v1/coupons/campaigns\"\n    coupon_code = f\"E2ETEST{int(datetime.now().timestamp())}\"\n    payload = {\n        \"discountType\": \"AMOUNT\",\n        \"applyTarget\": \"ALL\",\n        \"validTerm\": 30,\n        \"startAt\": start,\n        \"endAt\": end,\n        \"issuedAmount\": 100,\n        \"discountAmount\": 1000,\n        \"couponCode\": coupon_code,\n        \"minimumCost\": 10000,\n    }\n    print(f\"\\n[test_create_coupon_campaign] POST {url}\")\n    print(f\"[test_create_coupon_campaign] payload={payload}\")\n    resp = requests.post(url, json=payload, headers=auth_headers)\n    print(f\"[test_create_coupon_campaign] status={resp.status_code}, body={resp.text[:400]}\")\n\n    if resp.status_code in (401, 403):\n        pytest.skip(\"SUPER_ADMIN 권한 부족으로 쿠폰 캠페인 생성을 건너뜁니다.\")\n\n    assert resp.status_code in (200, 201), (\n        f\"쿠폰 캠페인 생성 실패: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    _coupon_state[\"coupon_code\"] = coupon_code\n    print(f\"[test_create_coupon_campaign] 쿠폰 캠페인 생성 완료: code={coupon_code}\")\n\n\ndef test_issue_coupon(base_url, auth_headers):\n    \"\"\"생성된 쿠폰 캠페인에서 쿠폰을 발급받습니다.\"\"\"\n    coupon_code = _coupon_state.get(\"coupon_code\")\n    if not coupon_code:\n        pytest.skip(\"coupon_code가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/coupons/campaigns/{coupon_code}\"\n    print(f\"\\n[test_issue_coupon] POST {url}\")\n    resp = requests.post(url, headers=auth_headers)\n    print(f\"[test_issue_coupon] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (200, 201), (\n        f\"쿠폰 발급 실패: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    data = get_data(resp)\n    if \"issuedCouponId\" in data:\n        _coupon_state[\"coupon_id\"] = data[\"issuedCouponId\"]\n    elif \"couponId\" in data:\n        _coupon_state[\"coupon_id\"] = data[\"couponId\"]\n    print(f\"[test_issue_coupon] 쿠폰 발급 완료: id={_coupon_state['coupon_id']}\")\n\n\ndef test_get_my_coupons(base_url, auth_headers):\n    \"\"\"발급받은 쿠폰 목록을 조회합니다.\"\"\"\n    url = f\"{base_url}/v1/coupons\"\n    params = {\"expired\": \"false\"}\n    print(f\"\\n[test_get_my_coupons] GET {url}\")\n    resp = requests.get(url, params=params, headers=auth_headers)\n    print(f\"[test_get_my_coupons] status={resp.status_code}, body={resp.text[:500]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    print(f\"[test_get_my_coupons] 쿠폰 목록 조회 완료: {data}\")\n\n\ndef test_order_with_coupon(base_url, auth_headers, state):\n    \"\"\"쿠폰을 적용하여 주문을 생성합니다 (할인 적용 검증).\"\"\"\n    coupon_id = _coupon_state.get(\"coupon_id\")\n    if not coupon_id:\n        pytest.skip(\"coupon_id가 없어 테스트를 건너뜁니다.\")\n    if not state.ticket_item_id:\n        pytest.skip(\"ticket_item_id가 없어 테스트를 건너뜁니다.\")\n\n    # 장바구니 생성\n    cart_resp = requests.post(\n        f\"{base_url}/v1/carts\",\n        json={\"items\": [{\"itemId\": state.ticket_item_id, \"quantity\": 1, \"options\": []}]},\n        headers=auth_headers,\n    )\n    if cart_resp.status_code not in (200, 201):\n        pytest.skip(f\"장바구니 생성 실패: {cart_resp.text[:200]}\")\n    cart_id = get_data(cart_resp)[\"cartId\"]\n\n    # 쿠폰 적용 주문 생성\n    order_url = f\"{base_url}/v1/orders/\"\n    order_payload = {\"couponId\": coupon_id, \"cartId\": cart_id}\n    print(f\"\\n[test_order_with_coupon] POST {order_url} (couponId={coupon_id})\")\n    order_resp = requests.post(order_url, json=order_payload, headers=auth_headers)\n    print(f\"[test_order_with_coupon] status={order_resp.status_code}, body={order_resp.text[:500]}\")\n\n    # 무료 티켓에 쿠폰 적용은 minimumCost 미달로 실패할 수 있음 (정상)\n    if order_resp.status_code in (400, 422):\n        print(\"[test_order_with_coupon] 무료 티켓+쿠폰 조합은 최소금액 미달로 예상대로 실패\")\n        return\n\n    assert order_resp.status_code in (200, 201), (\n        f\"쿠폰 적용 주문 실패: {order_resp.text[:300]}\"\n    )\n    data = get_data(order_resp)\n    print(f\"[test_order_with_coupon] 쿠폰 적용 주문 생성: {data.get('orderId')}\")\n"
  },
  {
    "path": "e2e-tests/test_26_order_edge_cases.py",
    "content": "\"\"\"\n주문 엣지 케이스 E2E 테스트.\n재고 소진, 타 유저 주문 조회 차단, 중복 결제 방지 등을 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import get_data\n\n\n# 이 모듈 내에서만 사용하는 로컬 상태\n_edge_state: dict = {\n    \"exhausted_ticket_id\": 0,\n    \"exhausted_event_id\": 0,\n    \"user_a_headers\": {},\n    \"user_a_order_uuid\": \"\",\n    \"user_b_headers\": {},\n}\n\n\ndef _login(base_url: str, email: str, name: str) -> dict:\n    \"\"\"테스트 유저 로그인 헬퍼.\"\"\"\n    resp = requests.post(\n        f\"{base_url}/v1/auth/oauth/local/login\",\n        json={\n            \"email\": email,\n            \"name\": name,\n            \"phoneNumber\": \"010-0000-0000\",\n            \"profileImage\": None,\n            \"marketingAgree\": False,\n        },\n    )\n    assert resp.status_code == 200, f\"로그인 실패: {resp.text}\"\n    return get_data(resp)\n\n\ndef test_setup_stock_exhaustion(base_url, auth_headers, state):\n    \"\"\"재고 1개 티켓을 생성하고 소진시킵니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    from datetime import datetime, timedelta\n    future = (datetime.now() + timedelta(days=120)).strftime(\"%Y.%m.%d %H:%M\")\n\n    # 새 이벤트 생성\n    event_resp = requests.post(\n        f\"{base_url}/v1/events\",\n        json={\"hostId\": state.host_id, \"name\": \"재고소진테스트이벤트\", \"startAt\": future, \"runTime\": 60},\n        headers=auth_headers,\n    )\n    if event_resp.status_code not in (200, 201):\n        pytest.skip(f\"이벤트 생성 실패: {event_resp.text[:200]}\")\n    _edge_state[\"exhausted_event_id\"] = get_data(event_resp)[\"eventId\"]\n\n    # 기본 정보 설정 (장소 포함)\n    requests.patch(\n        f\"{base_url}/v1/events/{_edge_state['exhausted_event_id']}/basic\",\n        json={\"name\": \"재고소진테스트이벤트\", \"startAt\": future, \"runTime\": 60,\n              \"placeName\": \"재고테스트공연장\", \"placeAddress\": \"서울시 강남구\",\n              \"longitude\": 127.0, \"latitude\": 37.5},\n        headers=auth_headers,\n    )\n\n    # 상세 정보 설정\n    requests.patch(\n        f\"{base_url}/v1/events/{_edge_state['exhausted_event_id']}/details\",\n        json={\"posterImageKey\": \"test/event/e2e/poster.jpeg\", \"content\": \"재고 소진 테스트\"},\n        headers=auth_headers,\n    )\n\n    # 재고 1개 티켓 생성\n    ticket_resp = requests.post(\n        f\"{base_url}/v1/events/{_edge_state['exhausted_event_id']}/ticketItems\",\n        json={\n            \"payType\": \"무료티켓\",\n            \"name\": \"재고1개티켓\",\n            \"description\": \"재고가 1개뿐인 티켓\",\n            \"price\": 0,\n            \"supplyCount\": 1,\n            \"approveType\": \"선착순\",\n            \"isQuantityPublic\": True,\n            \"purchaseLimit\": 1,\n        },\n        headers=auth_headers,\n    )\n    assert ticket_resp.status_code in (200, 201), f\"티켓 생성 실패: {ticket_resp.text[:300]}\"\n    _edge_state[\"exhausted_ticket_id\"] = get_data(ticket_resp)[\"ticketItemId\"]\n\n    # 이벤트 오픈\n    open_resp = requests.patch(\n        f\"{base_url}/v1/events/{_edge_state['exhausted_event_id']}/open\",\n        headers=auth_headers,\n    )\n    assert open_resp.status_code == 200, f\"이벤트 오픈 실패: {open_resp.text[:200]}\"\n\n    # userA로 재고 소진\n    data_a = _login(base_url, \"stock-userA@dudoong.com\", \"재고테스터A\")\n    _edge_state[\"user_a_headers\"] = {\"Authorization\": f\"Bearer {data_a['accessToken']}\"}\n\n    cart_resp = requests.post(\n        f\"{base_url}/v1/carts\",\n        json={\"items\": [{\"itemId\": _edge_state[\"exhausted_ticket_id\"], \"quantity\": 1, \"options\": []}]},\n        headers=_edge_state[\"user_a_headers\"],\n    )\n    assert cart_resp.status_code in (200, 201)\n    cart_id = get_data(cart_resp)[\"cartId\"]\n\n    order_resp = requests.post(\n        f\"{base_url}/v1/orders/\",\n        json={\"couponId\": None, \"cartId\": cart_id},\n        headers=_edge_state[\"user_a_headers\"],\n    )\n    assert order_resp.status_code in (200, 201)\n    order_uuid = get_data(order_resp)[\"orderId\"]\n    _edge_state[\"user_a_order_uuid\"] = order_uuid\n\n    free_resp = requests.post(\n        f\"{base_url}/v1/orders/{order_uuid}/free\",\n        headers=_edge_state[\"user_a_headers\"],\n    )\n    assert free_resp.status_code == 200, f\"무료결제 실패: {free_resp.text[:200]}\"\n    print(f\"[test_setup_stock_exhaustion] 재고 소진 완료: ticket={_edge_state['exhausted_ticket_id']}\")\n\n\ndef test_order_after_stock_exhausted(base_url):\n    \"\"\"재고 소진 후 주문을 시도하면 실패합니다.\"\"\"\n    ticket_id = _edge_state.get(\"exhausted_ticket_id\")\n    if not ticket_id:\n        pytest.skip(\"재고 소진 셋업이 안 됨\")\n\n    data_b = _login(base_url, \"stock-userB@dudoong.com\", \"재고테스터B\")\n    _edge_state[\"user_b_headers\"] = {\"Authorization\": f\"Bearer {data_b['accessToken']}\"}\n\n    # 장바구니 생성 시도\n    cart_resp = requests.post(\n        f\"{base_url}/v1/carts\",\n        json={\"items\": [{\"itemId\": ticket_id, \"quantity\": 1, \"options\": []}]},\n        headers=_edge_state[\"user_b_headers\"],\n    )\n    print(f\"\\n[test_order_after_stock_exhausted] cart status={cart_resp.status_code}\")\n\n    if cart_resp.status_code in (400, 409, 422):\n        print(f\"[test_order_after_stock_exhausted] 장바구니 단계에서 재고 부족 감지: {cart_resp.status_code}\")\n        return\n\n    if cart_resp.status_code not in (200, 201):\n        print(f\"[test_order_after_stock_exhausted] 장바구니 실패: {cart_resp.status_code}\")\n        return\n\n    cart_id = get_data(cart_resp)[\"cartId\"]\n\n    # 주문 생성 시도\n    order_resp = requests.post(\n        f\"{base_url}/v1/orders/\",\n        json={\"couponId\": None, \"cartId\": cart_id},\n        headers=_edge_state[\"user_b_headers\"],\n    )\n    print(f\"[test_order_after_stock_exhausted] order status={order_resp.status_code}\")\n\n    if order_resp.status_code in (400, 409, 422):\n        print(f\"[test_order_after_stock_exhausted] 주문 단계에서 재고 부족 감지: {order_resp.status_code}\")\n        return\n\n    if order_resp.status_code not in (200, 201):\n        return\n\n    order_uuid = get_data(order_resp)[\"orderId\"]\n    free_resp = requests.post(\n        f\"{base_url}/v1/orders/{order_uuid}/free\",\n        headers=_edge_state[\"user_b_headers\"],\n    )\n    print(f\"[test_order_after_stock_exhausted] free status={free_resp.status_code}\")\n    assert free_resp.status_code != 200, (\n        f\"재고 소진 후 주문이 성공해서는 안 됩니다: {free_resp.status_code}\"\n    )\n    print(f\"[test_order_after_stock_exhausted] 재고 부족 차단 확인: {free_resp.status_code}\")\n\n\ndef test_other_user_cannot_view_order(base_url):\n    \"\"\"다른 유저의 주문을 조회하면 차단됩니다.\"\"\"\n    order_uuid = _edge_state.get(\"user_a_order_uuid\")\n    if not order_uuid or not _edge_state.get(\"user_b_headers\"):\n        pytest.skip(\"테스트 데이터 없음\")\n\n    url = f\"{base_url}/v1/orders/{order_uuid}\"\n    print(f\"\\n[test_other_user_cannot_view_order] GET {url} (userB가 userA 주문 조회)\")\n    resp = requests.get(url, headers=_edge_state[\"user_b_headers\"])\n    print(f\"[test_other_user_cannot_view_order] status={resp.status_code}\")\n\n    assert resp.status_code in (400, 401, 403, 404), (\n        f\"타 유저 주문 조회가 차단되어야 합니다: status={resp.status_code}\"\n    )\n    print(f\"[test_other_user_cannot_view_order] 타 유저 주문 조회 차단: {resp.status_code}\")\n\n\ndef test_duplicate_free_payment_fails(base_url, auth_headers, state):\n    \"\"\"이미 결제된 주문에 다시 무료 결제를 시도하면 실패합니다.\"\"\"\n    if not state.order_uuid:\n        pytest.skip(\"order_uuid가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/orders/{state.order_uuid}/free\"\n    print(f\"\\n[test_duplicate_free_payment_fails] POST {url} (중복 결제 시도)\")\n    resp = requests.post(url, headers=auth_headers)\n    print(f\"[test_duplicate_free_payment_fails] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code != 200, (\n        f\"이미 결제된 주문의 중복 결제가 성공해서는 안 됩니다: {resp.status_code}\"\n    )\n    print(f\"[test_duplicate_free_payment_fails] 중복 결제 차단 확인: {resp.status_code}\")\n\n\ndef test_unauthenticated_order_attempt(base_url, state):\n    \"\"\"인증 없이 주문을 시도하면 401/403이 반환됩니다.\"\"\"\n    if not state.ticket_item_id:\n        pytest.skip(\"ticket_item_id가 없어 테스트를 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/carts\"\n    payload = {\"items\": [{\"itemId\": state.ticket_item_id, \"quantity\": 1, \"options\": []}]}\n    print(f\"\\n[test_unauthenticated_order_attempt] POST {url} (인증 없음)\")\n    resp = requests.post(url, json=payload)\n    print(f\"[test_unauthenticated_order_attempt] status={resp.status_code}\")\n\n    assert resp.status_code in (401, 403), (\n        f\"인증 없는 주문 시도가 차단되어야 합니다: status={resp.status_code}\"\n    )\n    print(f\"[test_unauthenticated_order_attempt] 미인증 차단: {resp.status_code}\")\n"
  },
  {
    "path": "e2e-tests/test_27_host_role_change.py",
    "content": "\"\"\"\n호스트 역할 변경 E2E 테스트.\n매니저 → 스태프 역할 변경 및 권한별 동작을 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\n_role_state: dict = {\n    \"user_manager_token\": \"\",\n    \"user_manager_headers\": {},\n    \"user_staff_token\": \"\",\n    \"user_staff_headers\": {},\n    \"host_id\": 0,\n    \"event_id\": 0,\n}\n\n\ndef _login(base_url: str, email: str, name: str) -> dict:\n    resp = requests.post(\n        f\"{base_url}/v1/auth/oauth/local/login\",\n        json={\n            \"email\": email,\n            \"name\": name,\n            \"phoneNumber\": \"010-0000-0000\",\n            \"profileImage\": None,\n            \"marketingAgree\": False,\n        },\n    )\n    assert resp.status_code == 200, f\"로그인 실패: {resp.text}\"\n    return get_data(resp)\n\n\ndef test_setup_host_for_role_change(base_url, auth_headers, state):\n    \"\"\"역할 변경 테스트를 위한 호스트 셋업 및 매니저 초대.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n    _role_state[\"host_id\"] = state.host_id\n\n    # 매니저용 유저 로그인\n    data = _login(base_url, \"role-manager@dudoong.com\", \"역할매니저\")\n    _role_state[\"user_manager_token\"] = data[\"accessToken\"]\n    _role_state[\"user_manager_headers\"] = {\"Authorization\": f\"Bearer {data['accessToken']}\"}\n\n    # 매니저로 초대\n    invite_resp = requests.post(\n        f\"{base_url}/v1/hosts/{state.host_id}/invite\",\n        json={\"email\": \"role-manager@dudoong.com\", \"role\": \"MANAGER\"},\n        headers=auth_headers,\n    )\n    if invite_resp.status_code not in (200, 201):\n        pytest.skip(f\"매니저 초대 실패: {invite_resp.text[:200]}\")\n\n    # 가입\n    join_resp = requests.post(\n        f\"{base_url}/v1/hosts/{state.host_id}/join\",\n        headers=_role_state[\"user_manager_headers\"],\n    )\n    assert join_resp.status_code in (200, 201), f\"가입 실패: {join_resp.text[:200]}\"\n    print(\"[setup] 매니저 역할 유저 호스트 가입 완료\")\n\n\ndef test_change_role_manager_to_guest(base_url, auth_headers):\n    \"\"\"마스터가 매니저를 GUEST로 역할 변경합니다.\"\"\"\n    host_id = _role_state.get(\"host_id\")\n    if not host_id or not _role_state[\"user_manager_headers\"]:\n        pytest.skip(\"셋업 안 됨\")\n\n    # 매니저 유저의 userId 조회\n    me_resp = requests.get(\n        f\"{base_url}/v1/users/me\",\n        headers=_role_state[\"user_manager_headers\"],\n    )\n    if me_resp.status_code != 200:\n        pytest.skip(f\"매니저 유저 정보 조회 실패: {me_resp.text[:200]}\")\n    me_data = get_data(me_resp)\n    manager_user_id = me_data.get(\"userId\", me_data.get(\"id\"))\n    if not manager_user_id:\n        pytest.skip(f\"매니저 userId 조회 실패: {me_data}\")\n\n    url = f\"{base_url}/v1/hosts/{host_id}/role\"\n    payload = {\"userId\": manager_user_id, \"role\": \"GUEST\"}\n    print(f\"\\n[test_change_role] PATCH {url} MANAGER -> GUEST (userId={manager_user_id})\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_change_role] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (200, 201), (\n        f\"역할 변경 실패: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    print(\"[test_change_role] MANAGER -> GUEST 변경 완료\")\n\n\ndef test_demoted_user_restricted_access(base_url):\n    \"\"\"GUEST로 변경된 유저가 호스트 관리 기능에 제한됩니다.\"\"\"\n    host_id = _role_state.get(\"host_id\")\n    if not host_id or not _role_state[\"user_manager_headers\"]:\n        pytest.skip(\"셋업 안 됨\")\n\n    # GUEST는 이벤트 생성이 불가해야 함\n    from datetime import datetime, timedelta\n    future = (datetime.now() + timedelta(days=200)).strftime(\"%Y.%m.%d %H:%M\")\n\n    url = f\"{base_url}/v1/events\"\n    resp = requests.post(\n        url,\n        json={\"hostId\": host_id, \"name\": \"게스트테스트이벤트\", \"startAt\": future, \"runTime\": 60},\n        headers=_role_state[\"user_manager_headers\"],\n    )\n    print(f\"\\n[test_demoted_user] GUEST 이벤트 생성 시도: status={resp.status_code}\")\n\n    # GUEST는 이벤트 생성 권한이 없어야 함 (400/403)\n    # 만약 200이면 서버에서 역할 검증 안 하는 것이므로 기록만 남김\n    if resp.status_code in (400, 403):\n        print(\"[test_demoted_user] GUEST 이벤트 생성 차단 확인\")\n    else:\n        print(f\"[test_demoted_user] GUEST 이벤트 생성 결과: {resp.status_code} (역할 검증 미구현일 수 있음)\")\n\n\ndef test_multiple_role_users_host_detail(base_url, auth_headers):\n    \"\"\"호스트 상세 조회 시 여러 역할의 멤버가 포함됩니다.\"\"\"\n    host_id = _role_state.get(\"host_id\")\n    if not host_id:\n        pytest.skip(\"host_id가 없어 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/hosts/{host_id}\"\n    print(f\"\\n[test_multiple_role_users] GET {url}\")\n    resp = requests.get(url, headers=auth_headers)\n    print(f\"[test_multiple_role_users] status={resp.status_code}, body={resp.text[:500]}\")\n\n    assert_status(resp, 200)\n    print(\"[test_multiple_role_users] 호스트 상세 조회 성공\")\n"
  },
  {
    "path": "e2e-tests/test_28_ticket_quantity_public.py",
    "content": "\"\"\"\n티켓 재고 공개/비공개 설정 검증 E2E 테스트.\nisQuantityPublic=true/false에 따른 응답 차이를 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\n_qty_state: dict = {\n    \"event_id\": 0,\n    \"public_ticket_id\": 0,\n    \"private_ticket_id\": 0,\n}\n\n\ndef test_setup_event_for_quantity_test(base_url, auth_headers, state):\n    \"\"\"재고 공개/비공개 테스트를 위한 이벤트 및 티켓 생성.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    from datetime import datetime, timedelta\n    future = (datetime.now() + timedelta(days=150)).strftime(\"%Y.%m.%d %H:%M\")\n\n    # 이벤트 생성\n    event_resp = requests.post(\n        f\"{base_url}/v1/events\",\n        json={\"hostId\": state.host_id, \"name\": \"재고공개테스트이벤트\", \"startAt\": future, \"runTime\": 60},\n        headers=auth_headers,\n    )\n    assert event_resp.status_code in (200, 201), f\"이벤트 생성 실패: {event_resp.text[:200]}\"\n    _qty_state[\"event_id\"] = get_data(event_resp)[\"eventId\"]\n\n    # 상세 설정\n    requests.patch(\n        f\"{base_url}/v1/events/{_qty_state['event_id']}/detail\",\n        json={\"content\": \"재고 공개/비공개 테스트\"},\n        headers=auth_headers,\n    )\n\n    # 재고 공개 티켓 (isQuantityPublic=True)\n    public_resp = requests.post(\n        f\"{base_url}/v1/events/{_qty_state['event_id']}/ticketItems\",\n        json={\n            \"payType\": \"무료티켓\",\n            \"name\": \"재고공개티켓\",\n            \"description\": \"재고가 공개되는 티켓\",\n            \"price\": 0,\n            \"supplyCount\": 100,\n            \"approveType\": \"선착순\",\n            \"isQuantityPublic\": True,\n            \"purchaseLimit\": 2,\n        },\n        headers=auth_headers,\n    )\n    assert public_resp.status_code in (200, 201), f\"공개 티켓 생성 실패: {public_resp.text[:300]}\"\n    _qty_state[\"public_ticket_id\"] = get_data(public_resp)[\"ticketItemId\"]\n\n    # 재고 비공개 티켓 (isQuantityPublic=False)\n    private_resp = requests.post(\n        f\"{base_url}/v1/events/{_qty_state['event_id']}/ticketItems\",\n        json={\n            \"payType\": \"무료티켓\",\n            \"name\": \"재고비공개티켓\",\n            \"description\": \"재고가 비공개되는 티켓\",\n            \"price\": 0,\n            \"supplyCount\": 50,\n            \"approveType\": \"선착순\",\n            \"isQuantityPublic\": False,\n            \"purchaseLimit\": 2,\n        },\n        headers=auth_headers,\n    )\n    assert private_resp.status_code in (200, 201), f\"비공개 티켓 생성 실패: {private_resp.text[:300]}\"\n    _qty_state[\"private_ticket_id\"] = get_data(private_resp)[\"ticketItemId\"]\n    print(f\"[setup] 공개={_qty_state['public_ticket_id']}, 비공개={_qty_state['private_ticket_id']}\")\n\n\ndef test_public_ticket_shows_quantity(base_url, state):\n    \"\"\"재고 공개 티켓은 재고 수량 정보가 응답에 포함됩니다.\"\"\"\n    event_id = _qty_state.get(\"event_id\")\n    if not event_id:\n        pytest.skip(\"셋업 안 됨\")\n\n    url = f\"{base_url}/v1/events/{event_id}/ticketItems\"\n    print(f\"\\n[test_public_ticket] GET {url}\")\n    resp = requests.get(url)\n    print(f\"[test_public_ticket] status={resp.status_code}, body={resp.text[:600]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n\n    # 티켓 목록에서 공개 티켓 찾기\n    tickets = data if isinstance(data, list) else data.get(\"ticketItems\", data.get(\"content\", []))\n    public_ticket = None\n    for t in tickets:\n        tid = t.get(\"ticketItemId\", t.get(\"id\"))\n        if tid == _qty_state[\"public_ticket_id\"]:\n            public_ticket = t\n            break\n\n    if public_ticket:\n        print(f\"[test_public_ticket] 공개 티켓 응답: {public_ticket}\")\n        # isQuantityPublic=True인 경우 수량 관련 필드가 존재해야 함\n        has_quantity = (\n            \"quantity\" in public_ticket\n            or \"supplyCount\" in public_ticket\n            or \"remainingCount\" in public_ticket\n            or \"stock\" in public_ticket\n        )\n        print(f\"[test_public_ticket] 수량 필드 존재: {has_quantity}\")\n    else:\n        print(\"[test_public_ticket] 공개 티켓을 목록에서 찾지 못함 (비로그인 조회 제한일 수 있음)\")\n\n\ndef test_private_ticket_hides_quantity(base_url):\n    \"\"\"재고 비공개 티켓은 재고 수량이 숨겨집니다.\"\"\"\n    event_id = _qty_state.get(\"event_id\")\n    if not event_id:\n        pytest.skip(\"셋업 안 됨\")\n\n    url = f\"{base_url}/v1/events/{event_id}/ticketItems\"\n    print(f\"\\n[test_private_ticket] GET {url}\")\n    resp = requests.get(url)\n    print(f\"[test_private_ticket] status={resp.status_code}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n\n    tickets = data if isinstance(data, list) else data.get(\"ticketItems\", data.get(\"content\", []))\n    private_ticket = None\n    for t in tickets:\n        tid = t.get(\"ticketItemId\", t.get(\"id\"))\n        if tid == _qty_state[\"private_ticket_id\"]:\n            private_ticket = t\n            break\n\n    if private_ticket:\n        print(f\"[test_private_ticket] 비공개 티켓 응답: {private_ticket}\")\n        # isQuantityPublic 필드가 false인지 확인\n        is_public = private_ticket.get(\"isQuantityPublic\", private_ticket.get(\"quantityPublic\"))\n        if is_public is not None:\n            assert is_public is False, f\"비공개 티켓의 isQuantityPublic이 True: {private_ticket}\"\n            print(\"[test_private_ticket] isQuantityPublic=false 확인\")\n    else:\n        print(\"[test_private_ticket] 비공개 티켓을 목록에서 찾지 못함\")\n\n\ndef test_both_ticket_types_in_same_event(base_url):\n    \"\"\"같은 이벤트에 재고 공개/비공개 티켓이 공존합니다.\"\"\"\n    event_id = _qty_state.get(\"event_id\")\n    if not event_id:\n        pytest.skip(\"셋업 안 됨\")\n\n    url = f\"{base_url}/v1/events/{event_id}/ticketItems\"\n    resp = requests.get(url)\n    assert_status(resp, 200)\n    data = get_data(resp)\n\n    tickets = data if isinstance(data, list) else data.get(\"ticketItems\", data.get(\"content\", []))\n    ticket_ids = [t.get(\"ticketItemId\", t.get(\"id\")) for t in tickets]\n\n    assert _qty_state[\"public_ticket_id\"] in ticket_ids, \"공개 티켓이 목록에 없습니다\"\n    assert _qty_state[\"private_ticket_id\"] in ticket_ids, \"비공개 티켓이 목록에 없습니다\"\n    print(f\"[test_both_types] 같은 이벤트에 공개/비공개 티켓 공존 확인: {ticket_ids}\")\n"
  },
  {
    "path": "e2e-tests/test_29_order_cancel.py",
    "content": "\"\"\"\n승인 대기 주문 취소 + 재고 복원 E2E 테스트.\n두둥티켓 주문을 생성 후 구매자가 취소하고, 재고가 복원되는지 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\n_cancel_state: dict = {\n    \"event_id\": 0,\n    \"ticket_item_id\": 0,\n    \"order_uuid\": \"\",\n    \"buyer_headers\": {},\n    \"initial_stock\": 0,\n}\n\n\ndef _login(base_url: str, email: str, name: str) -> dict:\n    resp = requests.post(\n        f\"{base_url}/v1/auth/oauth/local/login\",\n        json={\n            \"email\": email,\n            \"name\": name,\n            \"phoneNumber\": \"010-0000-0000\",\n            \"profileImage\": None,\n            \"marketingAgree\": False,\n        },\n    )\n    assert resp.status_code == 200, f\"로그인 실패: {resp.text}\"\n    return get_data(resp)\n\n\ndef test_setup_cancel_scenario(base_url, auth_headers, state):\n    \"\"\"주문 취소 테스트를 위한 두둥티켓 이벤트 셋업.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    from datetime import datetime, timedelta\n    future = (datetime.now() + timedelta(days=180)).strftime(\"%Y.%m.%d %H:%M\")\n\n    # 이벤트 생성\n    event_resp = requests.post(\n        f\"{base_url}/v1/events\",\n        json={\"hostId\": state.host_id, \"name\": \"주문취소테스트공연\", \"startAt\": future, \"runTime\": 90},\n        headers=auth_headers,\n    )\n    assert event_resp.status_code in (200, 201), f\"이벤트 생성 실패: {event_resp.text[:200]}\"\n    _cancel_state[\"event_id\"] = get_data(event_resp)[\"eventId\"]\n\n    # 기본 정보 설정 (장소 포함)\n    requests.patch(\n        f\"{base_url}/v1/events/{_cancel_state['event_id']}/basic\",\n        json={\"name\": \"주문취소테스트공연\", \"startAt\": future, \"runTime\": 90,\n              \"placeName\": \"취소테스트공연장\", \"placeAddress\": \"서울시 강남구\",\n              \"longitude\": 127.0, \"latitude\": 37.5},\n        headers=auth_headers,\n    )\n\n    # 상세 설정\n    requests.patch(\n        f\"{base_url}/v1/events/{_cancel_state['event_id']}/details\",\n        json={\"posterImageKey\": \"test/event/e2e/poster.jpeg\", \"content\": \"주문 취소 테스트 상세\"},\n        headers=auth_headers,\n    )\n\n    # 두둥티켓 생성 (승인 방식, 재고 10개)\n    ticket_resp = requests.post(\n        f\"{base_url}/v1/events/{_cancel_state['event_id']}/ticketItems\",\n        json={\n            \"payType\": \"두둥티켓\",\n            \"name\": \"취소테스트두둥티켓\",\n            \"description\": \"취소 테스트용 두둥티켓\",\n            \"price\": 5000,\n            \"supplyCount\": 10,\n            \"approveType\": \"승인\",\n            \"isQuantityPublic\": True,\n            \"purchaseLimit\": 2,\n            \"bankName\": \"신한\",\n            \"accountNumber\": \"110-123-456789\",\n            \"accountHolder\": \"테스터\",\n        },\n        headers=auth_headers,\n    )\n    assert ticket_resp.status_code in (200, 201), f\"티켓 생성 실패: {ticket_resp.text[:300]}\"\n    _cancel_state[\"ticket_item_id\"] = get_data(ticket_resp)[\"ticketItemId\"]\n    _cancel_state[\"initial_stock\"] = 10\n\n    # 이벤트 오픈\n    open_resp = requests.patch(\n        f\"{base_url}/v1/events/{_cancel_state['event_id']}/open\",\n        headers=auth_headers,\n    )\n    assert open_resp.status_code == 200, f\"이벤트 오픈 실패: {open_resp.text[:200]}\"\n\n    # 구매자 로그인\n    buyer_data = _login(base_url, \"cancel-buyer@dudoong.com\", \"취소구매자\")\n    _cancel_state[\"buyer_headers\"] = {\"Authorization\": f\"Bearer {buyer_data['accessToken']}\"}\n    print(f\"[setup] 취소 테스트 셋업 완료: event={_cancel_state['event_id']}, ticket={_cancel_state['ticket_item_id']}\")\n\n\ndef test_create_order_for_cancel(base_url):\n    \"\"\"취소할 두둥티켓 주문을 생성합니다.\"\"\"\n    if not _cancel_state[\"ticket_item_id\"] or not _cancel_state[\"buyer_headers\"]:\n        pytest.skip(\"셋업 안 됨\")\n\n    # 장바구니\n    cart_resp = requests.post(\n        f\"{base_url}/v1/carts\",\n        json={\"items\": [{\"itemId\": _cancel_state[\"ticket_item_id\"], \"quantity\": 1, \"options\": []}]},\n        headers=_cancel_state[\"buyer_headers\"],\n    )\n    assert cart_resp.status_code in (200, 201), f\"장바구니 실패: {cart_resp.text[:200]}\"\n    cart_id = get_data(cart_resp)[\"cartId\"]\n\n    # 주문 생성\n    order_resp = requests.post(\n        f\"{base_url}/v1/orders/\",\n        json={\"couponId\": None, \"cartId\": cart_id},\n        headers=_cancel_state[\"buyer_headers\"],\n    )\n    assert order_resp.status_code in (200, 201), f\"주문 실패: {order_resp.text[:200]}\"\n    _cancel_state[\"order_uuid\"] = get_data(order_resp)[\"orderId\"]\n    print(f\"[test_create_order] 주문 생성: {_cancel_state['order_uuid']}\")\n\n\ndef test_cancel_pending_order(base_url, auth_headers):\n    \"\"\"승인 대기 상태의 주문을 호스트(어드민)가 거절(refuse)합니다.\n    PENDING_APPROVE 상태는 refuse 엔드포인트로 취소해야 합니다.\"\"\"\n    order_uuid = _cancel_state.get(\"order_uuid\")\n    if not order_uuid:\n        pytest.skip(\"주문이 없어 건너뜁니다.\")\n\n    event_id = _cancel_state.get(\"event_id\")\n    url = f\"{base_url}/v1/events/{event_id}/orders/{order_uuid}/refuse\"\n    print(f\"\\n[test_cancel_pending] POST {url}\")\n    resp = requests.post(url, headers=auth_headers)\n    print(f\"[test_cancel_pending] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (200, 201), (\n        f\"주문 취소 실패: status={resp.status_code}, body={resp.text[:300]}\"\n    )\n    print(\"[test_cancel_pending] 승인 대기 주문 취소 완료\")\n\n\ndef test_cancelled_order_status(base_url):\n    \"\"\"취소된 주문의 상태가 취소로 변경되었는지 확인합니다.\"\"\"\n    order_uuid = _cancel_state.get(\"order_uuid\")\n    if not order_uuid or not _cancel_state[\"buyer_headers\"]:\n        pytest.skip(\"주문이 없어 건너뜁니다.\")\n\n    url = f\"{base_url}/v1/orders/{order_uuid}\"\n    print(f\"\\n[test_cancelled_status] GET {url}\")\n    resp = requests.get(url, headers=_cancel_state[\"buyer_headers\"])\n    print(f\"[test_cancelled_status] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n    # orderStatus는 top-level 또는 paymentInfo 안에 있을 수 있음\n    order_status = (\n        data.get(\"orderStatus\")\n        or (data.get(\"paymentInfo\") or {}).get(\"orderStatus\")\n        or \"\"\n    )\n    print(f\"[test_cancelled_status] 주문 상태: {order_status}\")\n    # 취소 상태 확인 (CANCELED, CANCELLED, CANCEL, 취소 등)\n    if order_status:\n        assert \"CANCEL\" in order_status.upper() or \"취소\" in order_status, (\n            f\"취소된 주문의 상태가 CANCEL이 아닙니다: {order_status}\"\n        )\n\n\ndef test_stock_restored_after_cancel(base_url):\n    \"\"\"주문 취소 후 재고가 복원되었는지 확인합니다.\"\"\"\n    event_id = _cancel_state.get(\"event_id\")\n    ticket_id = _cancel_state.get(\"ticket_item_id\")\n    if not event_id or not ticket_id:\n        pytest.skip(\"셋업 안 됨\")\n\n    url = f\"{base_url}/v1/events/{event_id}/ticketItems\"\n    print(f\"\\n[test_stock_restored] GET {url}\")\n    resp = requests.get(url)\n    print(f\"[test_stock_restored] status={resp.status_code}, body={resp.text[:600]}\")\n\n    assert_status(resp, 200)\n    data = get_data(resp)\n\n    tickets = data if isinstance(data, list) else data.get(\"ticketItems\", data.get(\"content\", []))\n    target = None\n    for t in tickets:\n        tid = t.get(\"ticketItemId\", t.get(\"id\"))\n        if tid == ticket_id:\n            target = t\n            break\n\n    if target:\n        supply = target.get(\"supplyCount\", target.get(\"quantity\", 0))\n        remaining = target.get(\"remainingCount\", target.get(\"stock\", supply))\n        print(f\"[test_stock_restored] supply={supply}, remaining={remaining}\")\n        # 취소 후 재고가 원래대로(10개) 복원되어야 함\n        assert remaining == _cancel_state[\"initial_stock\"], (\n            f\"재고 복원 실패: expected={_cancel_state['initial_stock']}, actual={remaining}\"\n        )\n        print(\"[test_stock_restored] 재고 복원 확인\")\n    else:\n        print(\"[test_stock_restored] 티켓을 찾지 못함\")\n"
  },
  {
    "path": "e2e-tests/test_30_order_before_open.py",
    "content": "\"\"\"\n오픈 전 이벤트 주문 시도 차단 E2E 테스트.\nPREPARING 상태 이벤트에 주문을 시도하면 실패하는지 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\n_preopen_state: dict = {\n    \"event_id\": 0,\n    \"ticket_item_id\": 0,\n    \"buyer_headers\": {},\n}\n\n\ndef _login(base_url: str, email: str, name: str) -> dict:\n    resp = requests.post(\n        f\"{base_url}/v1/auth/oauth/local/login\",\n        json={\n            \"email\": email,\n            \"name\": name,\n            \"phoneNumber\": \"010-0000-0000\",\n            \"profileImage\": None,\n            \"marketingAgree\": False,\n        },\n    )\n    assert resp.status_code == 200, f\"로그인 실패: {resp.text}\"\n    return get_data(resp)\n\n\ndef test_setup_preparing_event(base_url, auth_headers, state):\n    \"\"\"PREPARING 상태(오픈 전)인 이벤트와 티켓을 생성합니다. 오픈하지 않습니다.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    from datetime import datetime, timedelta\n    future = (datetime.now() + timedelta(days=200)).strftime(\"%Y.%m.%d %H:%M\")\n\n    # 이벤트 생성 (PREPARING 상태)\n    event_resp = requests.post(\n        f\"{base_url}/v1/events\",\n        json={\"hostId\": state.host_id, \"name\": \"오픈전주문차단테스트\", \"startAt\": future, \"runTime\": 60},\n        headers=auth_headers,\n    )\n    assert event_resp.status_code in (200, 201), f\"이벤트 생성 실패: {event_resp.text[:200]}\"\n    _preopen_state[\"event_id\"] = get_data(event_resp)[\"eventId\"]\n\n    # 기본 정보 및 상세 설정\n    requests.patch(\n        f\"{base_url}/v1/events/{_preopen_state['event_id']}/basic\",\n        json={\"name\": \"오픈전주문차단테스트\", \"startAt\": future, \"runTime\": 60,\n              \"placeName\": \"테스트공연장\", \"placeAddress\": \"서울시 강남구\",\n              \"longitude\": 127.0, \"latitude\": 37.5},\n        headers=auth_headers,\n    )\n    requests.patch(\n        f\"{base_url}/v1/events/{_preopen_state['event_id']}/details\",\n        json={\"posterImageKey\": \"test/event/e2e/poster.jpeg\", \"content\": \"오픈 전 주문 차단 테스트\"},\n        headers=auth_headers,\n    )\n\n    # 무료 티켓 생성\n    ticket_resp = requests.post(\n        f\"{base_url}/v1/events/{_preopen_state['event_id']}/ticketItems\",\n        json={\n            \"payType\": \"무료티켓\",\n            \"name\": \"오픈전테스트티켓\",\n            \"description\": \"오픈 전 상태의 티켓\",\n            \"price\": 0,\n            \"supplyCount\": 50,\n            \"approveType\": \"선착순\",\n            \"isQuantityPublic\": True,\n            \"purchaseLimit\": 2,\n        },\n        headers=auth_headers,\n    )\n    assert ticket_resp.status_code in (200, 201), f\"티켓 생성 실패: {ticket_resp.text[:300]}\"\n    _preopen_state[\"ticket_item_id\"] = get_data(ticket_resp)[\"ticketItemId\"]\n\n    # 구매자 로그인 (이벤트는 오픈하지 않음!)\n    buyer_data = _login(base_url, \"preopen-buyer@dudoong.com\", \"오픈전구매자\")\n    _preopen_state[\"buyer_headers\"] = {\"Authorization\": f\"Bearer {buyer_data['accessToken']}\"}\n\n    print(f\"[setup] PREPARING 이벤트 셋업 완료: event={_preopen_state['event_id']} (오픈 안 함)\")\n\n\ndef test_cart_before_open_fails(base_url):\n    \"\"\"오픈 전 이벤트의 티켓으로 장바구니를 생성하면 실패합니다.\"\"\"\n    ticket_id = _preopen_state.get(\"ticket_item_id\")\n    if not ticket_id or not _preopen_state[\"buyer_headers\"]:\n        pytest.skip(\"셋업 안 됨\")\n\n    url = f\"{base_url}/v1/carts\"\n    payload = {\"items\": [{\"itemId\": ticket_id, \"quantity\": 1, \"options\": []}]}\n    print(f\"\\n[test_cart_before_open] POST {url}\")\n    resp = requests.post(url, json=payload, headers=_preopen_state[\"buyer_headers\"])\n    print(f\"[test_cart_before_open] status={resp.status_code}, body={resp.text[:400]}\")\n\n    # 장바구니 단계에서 차단되거나, 주문 단계에서 차단될 수 있음\n    if resp.status_code in (400, 403, 409, 422):\n        print(f\"[test_cart_before_open] 장바구니 단계에서 오픈 전 차단 확인: {resp.status_code}\")\n        return\n\n    if resp.status_code not in (200, 201):\n        print(f\"[test_cart_before_open] 장바구니 실패: {resp.status_code}\")\n        return\n\n    # 장바구니가 성공하면 주문 단계에서 차단되는지 확인\n    cart_id = get_data(resp)[\"cartId\"]\n    order_resp = requests.post(\n        f\"{base_url}/v1/orders/\",\n        json={\"couponId\": None, \"cartId\": cart_id},\n        headers=_preopen_state[\"buyer_headers\"],\n    )\n    print(f\"[test_cart_before_open] order status={order_resp.status_code}\")\n\n    if order_resp.status_code in (400, 403, 409, 422):\n        print(f\"[test_cart_before_open] 주문 단계에서 오픈 전 차단 확인: {order_resp.status_code}\")\n        return\n\n    if order_resp.status_code not in (200, 201):\n        return\n\n    # 주문도 성공하면 결제 단계에서라도 차단되어야 함\n    order_uuid = get_data(order_resp)[\"orderId\"]\n    free_resp = requests.post(\n        f\"{base_url}/v1/orders/{order_uuid}/free\",\n        headers=_preopen_state[\"buyer_headers\"],\n    )\n    print(f\"[test_cart_before_open] free status={free_resp.status_code}\")\n    assert free_resp.status_code != 200, (\n        f\"오픈 전 이벤트의 주문이 완료되어서는 안 됩니다: {free_resp.status_code}\"\n    )\n    print(f\"[test_cart_before_open] 결제 단계에서 오픈 전 차단 확인: {free_resp.status_code}\")\n\n\ndef test_event_still_preparing(base_url):\n    \"\"\"이벤트가 여전히 PREPARING 상태인지 확인합니다.\"\"\"\n    event_id = _preopen_state.get(\"event_id\")\n    if not event_id:\n        pytest.skip(\"셋업 안 됨\")\n\n    url = f\"{base_url}/v1/events/{event_id}\"\n    print(f\"\\n[test_event_still_preparing] GET {url}\")\n    resp = requests.get(url)\n    print(f\"[test_event_still_preparing] status={resp.status_code}, body={resp.text[:400]}\")\n\n    assert resp.status_code in (200, 400)\n    if resp.status_code == 400:\n        print(\"[test_event_still_preparing] 비오픈 이벤트 조회 제한 확인 (400)\")\n        return\n    data = get_data(resp)\n    status = data.get(\"status\", data.get(\"eventStatus\", \"\"))\n    print(f\"[test_event_still_preparing] 이벤트 상태: {status}\")\n    if status:\n        assert \"PREPARING\" in status.upper(), f\"이벤트가 PREPARING이 아닙니다: {status}\"\n"
  },
  {
    "path": "e2e-tests/test_31_admin_auth_role.py",
    "content": "\"\"\"\n어드민 역할 기반 접근 제어 E2E 테스트.\n일반 USER 역할로 어드민 API에 접근 시 403이 반환되는지 검증합니다.\n\n변경사항: 어드민 로컬 로그인 엔드포인트 삭제됨.\n일반 auth 엔드포인트로 로그인한 USER 토큰으로 admin API 접근을 테스트합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\n@pytest.fixture(scope=\"module\")\ndef admin_base_url():\n    \"\"\"어드민 API 베이스 URL.\"\"\"\n    import os\n    base = os.environ.get(\"API_BASE_URL\", \"http://localhost:8080/api\")\n    return base.replace(\"/api\", \"/internal-api\")\n\n\n@pytest.fixture(scope=\"module\")\ndef normal_user_token():\n    \"\"\"\n    일반 USER 역할로 로컬 로그인하여 토큰을 획득합니다.\n    일반 auth 엔드포인트로 로그인하여 토큰을 획득합니다.\n    \"\"\"\n    import os\n    base = os.environ.get(\"API_BASE_URL\", \"http://localhost:8080/api\")\n    url = f\"{base}/v1/auth/oauth/local/login\"\n    payload = {\n        \"email\": \"normaluser@dudoong.com\",\n        \"name\": \"일반유저\",\n        \"phoneNumber\": \"010-1234-5678\",\n        \"profileImage\": None,\n        \"marketingAgree\": False,\n    }\n    print(f\"\\n[normal_user_token] POST {url}\")\n    resp = requests.post(url, json=payload)\n    print(f\"[normal_user_token] status={resp.status_code}, body={resp.text[:300]}\")\n    assert resp.status_code == 200, f\"일반 유저 로그인 실패: {resp.text}\"\n    data = get_data(resp)\n    return data[\"accessToken\"]\n\n\n@pytest.fixture(scope=\"module\")\ndef normal_user_headers(normal_user_token):\n    \"\"\"일반 유저의 Authorization 헤더.\"\"\"\n    return {\"Authorization\": f\"Bearer {normal_user_token}\"}\n\n\ndef test_admin_dashboard_forbidden_for_user(admin_base_url, normal_user_headers):\n    \"\"\"일반 USER 토큰으로 어드민 대시보드 접근 시 403이 반환되는지 확인합니다.\"\"\"\n    url = f\"{admin_base_url}/v1/dashboard\"\n    print(f\"\\n[test_admin_dashboard_forbidden] GET {url}\")\n    resp = requests.get(url, headers=normal_user_headers)\n    print(f\"[test_admin_dashboard_forbidden] status={resp.status_code}\")\n\n    assert resp.status_code == 403, (\n        f\"일반 USER가 어드민 대시보드에 접근할 수 있습니다. status={resp.status_code}\"\n    )\n    print(\"[test_admin_dashboard_forbidden] 대시보드 접근 차단 확인\")\n\n\ndef test_admin_users_forbidden_for_user(admin_base_url, normal_user_headers):\n    \"\"\"일반 USER 토큰으로 어드민 유저 목록 접근 시 403이 반환되는지 확인합니다.\"\"\"\n    url = f\"{admin_base_url}/v1/users\"\n    print(f\"\\n[test_admin_users_forbidden] GET {url}\")\n    resp = requests.get(url, headers=normal_user_headers)\n    print(f\"[test_admin_users_forbidden] status={resp.status_code}\")\n\n    assert resp.status_code == 403, (\n        f\"일반 USER가 어드민 유저 목록에 접근할 수 있습니다. status={resp.status_code}\"\n    )\n    print(\"[test_admin_users_forbidden] 유저 목록 접근 차단 확인\")\n\n\ndef test_admin_events_forbidden_for_user(admin_base_url, normal_user_headers):\n    \"\"\"일반 USER 토큰으로 어드민 이벤트 목록 접근 시 403이 반환되는지 확인합니다.\"\"\"\n    url = f\"{admin_base_url}/v1/events\"\n    print(f\"\\n[test_admin_events_forbidden] GET {url}\")\n    resp = requests.get(url, headers=normal_user_headers)\n    print(f\"[test_admin_events_forbidden] status={resp.status_code}\")\n\n    assert resp.status_code == 403, (\n        f\"일반 USER가 어드민 이벤트 목록에 접근할 수 있습니다. status={resp.status_code}\"\n    )\n    print(\"[test_admin_events_forbidden] 이벤트 목록 접근 차단 확인\")\n\n\ndef test_admin_unauthenticated_access(admin_base_url):\n    \"\"\"토큰 없이 어드민 대시보드에 접근하면 401 또는 403이 반환되는지 확인합니다.\"\"\"\n    url = f\"{admin_base_url}/v1/dashboard\"\n    print(f\"\\n[test_admin_unauthenticated] GET {url} (토큰 없음)\")\n    resp = requests.get(url)\n    print(f\"[test_admin_unauthenticated] status={resp.status_code}\")\n\n    assert resp.status_code in (401, 403), (\n        f\"미인증 어드민 접근 시 401/403이 기대되지만 {resp.status_code}가 반환되었습니다\"\n    )\n    print(f\"[test_admin_unauthenticated] 미인증 접근 차단 확인: {resp.status_code}\")\n"
  },
  {
    "path": "e2e-tests/test_32_admin_token_separation.py",
    "content": "\"\"\"\nAdmin 토큰 분리 + 역할 기반 접근 제어 심층 E2E 테스트.\n\n쿠키 공유 방식 전환 후의 보안 정책을 검증합니다:\n1. 일반 유저 토큰(Authorization)으로 /internal-api/** 접근 불가 (403)\n2. 미인증 상태에서 admin API 접근 불가 (401/403)\n3. MANAGER 역할은 더 이상 admin 접근 불가 (ADMIN/SUPER_ADMIN만 허용)\n4. Admin /me 엔드포인트는 인증 필요\n\"\"\"\nimport os\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\n@pytest.fixture(scope=\"module\")\ndef api_base_url():\n    \"\"\"Public API 베이스 URL.\"\"\"\n    return os.environ.get(\"API_BASE_URL\", \"http://localhost:8080/api\")\n\n\n@pytest.fixture(scope=\"module\")\ndef admin_base_url(api_base_url):\n    \"\"\"Admin API 베이스 URL.\"\"\"\n    return api_base_url.replace(\"/api\", \"/internal-api\")\n\n\n@pytest.fixture(scope=\"module\")\ndef normal_user_token(api_base_url):\n    \"\"\"일반 USER 역할로 로그인하여 토큰 획득.\"\"\"\n    url = f\"{api_base_url}/v1/auth/oauth/local/login\"\n    payload = {\n        \"email\": \"rbac-normal@dudoong.com\",\n        \"name\": \"RBAC일반유저\",\n        \"phoneNumber\": \"010-1111-2222\",\n        \"profileImage\": None,\n        \"marketingAgree\": False,\n    }\n    resp = requests.post(url, json=payload)\n    assert resp.status_code == 200, f\"일반 유저 로그인 실패: {resp.text}\"\n    return get_data(resp)[\"accessToken\"]\n\n\n# ============================================================================\n# 1. 일반 유저 토큰 → Admin API 접근 차단\n# ============================================================================\n\nclass TestNormalUserTokenBlocked:\n    \"\"\"일반 유저 토큰(Authorization 헤더)으로 Admin API 접근이 차단되는지 검증.\"\"\"\n\n    @pytest.mark.parametrize(\"endpoint\", [\n        \"/v1/dashboard\",\n        \"/v1/users\",\n        \"/v1/events\",\n        \"/v1/orders\",\n        \"/v1/comments\",\n    ])\n    def test_authorization_header_blocked(self, admin_base_url, normal_user_token, endpoint):\n        \"\"\"일반 유저 토큰을 Authorization 헤더로 전달해도 admin API 차단.\"\"\"\n        url = f\"{admin_base_url}{endpoint}\"\n        headers = {\"Authorization\": f\"Bearer {normal_user_token}\"}\n        resp = requests.get(url, headers=headers)\n        assert resp.status_code == 403, (\n            f\"[Authorization] {endpoint}: 일반 유저 접근이 차단되지 않음. status={resp.status_code}\"\n        )\n\n    @pytest.mark.parametrize(\"endpoint\", [\n        \"/v1/dashboard\",\n        \"/v1/users\",\n        \"/v1/events\",\n    ])\n    def test_cookie_with_normal_token_blocked(self, admin_base_url, normal_user_token, endpoint):\n        \"\"\"일반 유저 토큰을 쿠키로 전달해도 admin API 차단 (ADMIN/SUPER_ADMIN 아님).\"\"\"\n        url = f\"{admin_base_url}{endpoint}\"\n        cookies = {\"accessToken\": normal_user_token}\n        resp = requests.get(url, cookies=cookies)\n        assert resp.status_code == 403, (\n            f\"[Cookie] {endpoint}: 일반 토큰으로 접근이 차단되지 않음. status={resp.status_code}\"\n        )\n\n\n# ============================================================================\n# 2. 미인증 접근 차단\n# ============================================================================\n\nclass TestUnauthenticatedBlocked:\n    \"\"\"토큰 없이 Admin API 접근이 차단되는지 검증.\"\"\"\n\n    @pytest.mark.parametrize(\"endpoint\", [\n        \"/v1/dashboard\",\n        \"/v1/users\",\n        \"/v1/events\",\n        \"/v1/orders\",\n        \"/v1/comments\",\n    ])\n    def test_no_token_blocked(self, admin_base_url, endpoint):\n        \"\"\"토큰 없이 admin API 접근 시 401 또는 403.\"\"\"\n        url = f\"{admin_base_url}{endpoint}\"\n        resp = requests.get(url)\n        assert resp.status_code in (401, 403), (\n            f\"{endpoint}: 미인증 접근 시 401/403이 기대되지만 {resp.status_code} 반환\"\n        )\n\n\n# ============================================================================\n# 3. Admin /me 엔드포인트는 인증 필요\n# ============================================================================\n\nclass TestAdminMeEndpoint:\n    \"\"\"Admin /me 엔드포인트는 ADMIN/SUPER_ADMIN 토큰이 필요.\"\"\"\n\n    def test_me_without_token_blocked(self, admin_base_url):\n        \"\"\"토큰 없이 /me 접근 시 차단.\"\"\"\n        url = f\"{admin_base_url}/v1/auth/me\"\n        resp = requests.get(url)\n        assert resp.status_code in (401, 403), (\n            f\"/me 미인증 접근 허용됨. status={resp.status_code}\"\n        )\n\n    def test_me_with_normal_token_blocked(self, admin_base_url, normal_user_token):\n        \"\"\"일반 유저 토큰으로 /me 접근 시 차단.\"\"\"\n        url = f\"{admin_base_url}/v1/auth/me\"\n        headers = {\"Authorization\": f\"Bearer {normal_user_token}\"}\n        resp = requests.get(url, headers=headers)\n        assert resp.status_code == 403, (\n            f\"일반 유저 토큰으로 admin /me 접근 허용됨. status={resp.status_code}\"\n        )\n"
  },
  {
    "path": "e2e-tests/test_33_host_role_authorization.py",
    "content": "\"\"\"\nHostRole AOP 호스트 권한 E2E 테스트.\n- 호스트 멤버가 아닌 유저는 호스트 관리 API 접근 불가\n- MASTER는 모든 호스트 관리 API 접근 가능\n- SUPER_ADMIN은 호스트 멤버가 아니어도 모든 접근 가능\n\nNOTE: DB에서 OID 기반으로 user_id를 직접 조회합니다 (me API userId 불일치 방지).\n\"\"\"\nimport os\nimport subprocess\nimport pytest\nimport requests\nfrom datetime import datetime, timedelta\nfrom conftest import assert_status, get_data\n\n\ndef _future_date_str(days_ahead=30):\n    future = datetime.now() + timedelta(days=days_ahead)\n    return future.strftime(\"%Y.%m.%d %H:%M\")\n\n\ndef mysql_query(sql):\n    \"\"\"MySQL 쿼리 실행 후 stdout 반환\"\"\"\n    result = subprocess.run(\n        [\"mysql\", \"-h\", \"127.0.0.1\", \"-P\", \"13306\", \"-u\", \"dudoong\", \"-pdudoong\",\n         \"dudoong\", \"-N\", \"-e\", sql],\n        capture_output=True, text=True\n    )\n    return result.stdout.strip()\n\n\ndef mysql_exec(sql):\n    \"\"\"MySQL 실행 (결과 불필요)\"\"\"\n    subprocess.run(\n        [\"mysql\", \"-h\", \"127.0.0.1\", \"-P\", \"13306\", \"-u\", \"dudoong\", \"-pdudoong\",\n         \"dudoong\", \"-e\", sql],\n        capture_output=True, text=True\n    )\n\n\ndef login_user(base_url, email, name):\n    \"\"\"로컬 로그인하여 (accessToken, jwt_user_id) 반환\"\"\"\n    import base64, json as _json\n    url = f\"{base_url}/v1/auth/oauth/local/login\"\n    payload = {\n        \"email\": email,\n        \"name\": name,\n        \"phoneNumber\": \"010-0000-0000\",\n        \"profileImage\": None,\n        \"marketingAgree\": False,\n    }\n    resp = requests.post(url, json=payload)\n    assert resp.status_code == 200, f\"로그인 실패: {resp.text}\"\n    token = get_data(resp)[\"accessToken\"]\n    # JWT sub에서 userId 추출\n    payload_part = token.split(\".\")[1]\n    payload_part += \"=\" * (4 - len(payload_part) % 4)\n    jwt_payload = _json.loads(base64.b64decode(payload_part))\n    user_id = int(jwt_payload[\"sub\"])\n    return token, user_id\n\n\n@pytest.fixture(scope=\"module\")\ndef api_base():\n    return os.environ.get(\"API_BASE_URL\", \"http://localhost:8080/api\")\n\n\n@pytest.fixture(scope=\"module\")\ndef user_a(api_base):\n    \"\"\"호스트 MASTER 유저\"\"\"\n    token, user_id = login_user(api_base, \"host-master-v2@dudoong.com\", \"호스트마스터\")\n    return {\"token\": token, \"user_id\": user_id, \"headers\": {\"Authorization\": f\"Bearer {token}\"}}\n\n\n@pytest.fixture(scope=\"module\")\ndef user_b(api_base):\n    \"\"\"외부 유저 (호스트 멤버 아님)\"\"\"\n    token, user_id = login_user(api_base, \"outsider-v2@dudoong.com\", \"외부유저\")\n    return {\"token\": token, \"user_id\": user_id, \"headers\": {\"Authorization\": f\"Bearer {token}\"}}\n\n\n@pytest.fixture(scope=\"module\")\ndef host_and_event(api_base, user_a):\n    \"\"\"유저 A로 호스트 + 이벤트 생성\"\"\"\n    # 호스트 생성\n    resp = requests.post(\n        f\"{api_base}/v1/hosts\",\n        json={\n            \"name\": \"E2E권한테스트호스트v2\",\n            \"contactEmail\": \"auth-test-v2@dudoong.com\",\n            \"contactNumber\": \"010-0000-0000\",\n        },\n        headers=user_a[\"headers\"],\n    )\n    assert_status(resp, 200)\n    host_id = get_data(resp).get(\"hostId\") or get_data(resp).get(\"id\")\n\n    # 이벤트 생성\n    resp = requests.post(\n        f\"{api_base}/v1/events\",\n        json={\n            \"name\": \"E2E권한테스트이벤트v2\",\n            \"hostId\": host_id,\n            \"startAt\": _future_date_str(30),\n            \"runTime\": 90,\n        },\n        headers=user_a[\"headers\"],\n    )\n    assert_status(resp, 200)\n    event_id = get_data(resp).get(\"eventId\") or get_data(resp).get(\"id\")\n\n    return {\"host_id\": host_id, \"event_id\": event_id}\n\n\nclass TestNonMemberBlocked:\n    \"\"\"호스트 멤버가 아닌 유저는 호스트 관리 API 접근 불가\"\"\"\n\n    def test_outsider_cannot_read_host_events(self, api_base, user_b, host_and_event):\n        \"\"\"외부 유저는 호스트 이벤트 목록 조회 불가\"\"\"\n        url = f\"{api_base}/v1/hosts/{host_and_event['host_id']}/events\"\n        resp = requests.get(url, headers=user_b[\"headers\"])\n        assert resp.status_code in (403, 400), (\n            f\"외부 유저가 호스트 이벤트에 접근 가능. status={resp.status_code}\"\n        )\n\n    def test_outsider_cannot_update_event(self, api_base, user_b, host_and_event):\n        \"\"\"외부 유저는 이벤트 수정 불가\"\"\"\n        url = f\"{api_base}/v1/events/{host_and_event['event_id']}/basic\"\n        resp = requests.patch(\n            url,\n            json={\"name\": \"해킹시도\"},\n            headers=user_b[\"headers\"],\n        )\n        assert resp.status_code in (403, 400), (\n            f\"외부 유저가 이벤트를 수정 가능. status={resp.status_code}\"\n        )\n\n\nclass TestMasterAccess:\n    \"\"\"호스트 MASTER는 모든 호스트 관리 API 접근 가능\"\"\"\n\n    def test_master_can_read_host_events(self, api_base, user_a, host_and_event):\n        \"\"\"MASTER는 호스트 이벤트 목록 조회 가능\"\"\"\n        url = f\"{api_base}/v1/hosts/{host_and_event['host_id']}/events\"\n        resp = requests.get(url, headers=user_a[\"headers\"])\n        assert_status(resp, 200)\n\n    def test_master_can_read_event_checklist(self, api_base, user_a, host_and_event):\n        \"\"\"MASTER는 이벤트 체크리스트 조회 가능\"\"\"\n        url = f\"{api_base}/v1/events/{host_and_event['event_id']}/checklist\"\n        resp = requests.get(url, headers=user_a[\"headers\"])\n        assert_status(resp, 200)\n\n\nclass TestSuperAdminBypass:\n    \"\"\"SUPER_ADMIN은 호스트 멤버가 아니어도 모든 접근 가능\"\"\"\n\n    def test_super_admin_can_access_any_host(self, api_base, user_b, host_and_event):\n        \"\"\"SUPER_ADMIN으로 승격된 유저는 아무 호스트에도 접근 가능\"\"\"\n        user_id = user_b[\"user_id\"]\n        if not user_id:\n            pytest.skip(\"유저 ID를 가져올 수 없음\")\n\n        # DB에서 SUPER_ADMIN으로 승격\n        mysql_exec(f\"UPDATE tbl_user SET account_role='SUPER_ADMIN' WHERE user_id={user_id}\")\n\n        try:\n            # 호스트 멤버가 아닌데 접근 가능해야 함\n            url = f\"{api_base}/v1/hosts/{host_and_event['host_id']}/events\"\n            resp = requests.get(url, headers=user_b[\"headers\"])\n            assert_status(resp, 200)\n\n            # 이벤트 체크리스트도 접근 가능\n            url = f\"{api_base}/v1/events/{host_and_event['event_id']}/checklist\"\n            resp = requests.get(url, headers=user_b[\"headers\"])\n            assert_status(resp, 200)\n        finally:\n            # DB 원복: USER로 되돌리기\n            mysql_exec(f\"UPDATE tbl_user SET account_role='USER' WHERE user_id={user_id}\")\n"
  },
  {
    "path": "e2e-tests/test_34_admin_usecase_authorization.py",
    "content": "\"\"\"\nAdmin UseCase 이중 권한 체크 E2E 테스트.\n- USER: admin API 접근 403\n- ADMIN: 읽기/쓰기 OK, 역할 변경 403\n- SUPER_ADMIN: 모든 API OK\n\nNOTE: DB에서 직접 user_id를 조회하여 role을 변경합니다 (me API의 userId와 DB user_id 불일치 방지).\n\"\"\"\nimport os\nimport subprocess\nimport pytest\nimport requests\nfrom conftest import assert_status, get_data\n\n\ndef mysql_query(sql):\n    \"\"\"MySQL 쿼리 실행 후 stdout 반환\"\"\"\n    result = subprocess.run(\n        [\"mysql\", \"-h\", \"127.0.0.1\", \"-P\", \"13306\", \"-u\", \"dudoong\", \"-pdudoong\",\n         \"dudoong\", \"-N\", \"-e\", sql],\n        capture_output=True, text=True\n    )\n    return result.stdout.strip()\n\n\ndef mysql_exec(sql):\n    \"\"\"MySQL 실행 (결과 불필요)\"\"\"\n    subprocess.run(\n        [\"mysql\", \"-h\", \"127.0.0.1\", \"-P\", \"13306\", \"-u\", \"dudoong\", \"-pdudoong\",\n         \"dudoong\", \"-e\", sql],\n        capture_output=True, text=True\n    )\n\n\n@pytest.fixture(scope=\"module\")\ndef api_base():\n    return os.environ.get(\"API_BASE_URL\", \"http://localhost:8080/api\")\n\n\n@pytest.fixture(scope=\"module\")\ndef admin_base(api_base):\n    return api_base.replace(\"/api\", \"/internal-api\")\n\n\n@pytest.fixture(scope=\"module\")\ndef test_user(api_base):\n    \"\"\"일반 로그인으로 토큰 획득 + DB에서 user_id를 이메일 기반으로 직접 조회\"\"\"\n    email = \"admin@dudoong.com\"\n    url = f\"{api_base}/v1/auth/oauth/local/login\"\n    payload = {\n        \"email\": email,\n        \"name\": \"어드민E2E\",\n        \"phoneNumber\": \"010-0000-0000\",\n        \"profileImage\": None,\n        \"marketingAgree\": False,\n    }\n    resp = requests.post(url, json=payload)\n    assert resp.status_code == 200, f\"로그인 실패: {resp.text}\"\n    token = get_data(resp)[\"accessToken\"]\n\n    # JWT sub에서 userId 추출\n    import base64, json\n    payload_part = token.split(\".\")[1]\n    payload_part += \"=\" * (4 - len(payload_part) % 4)\n    jwt_payload = json.loads(base64.b64decode(payload_part))\n    user_id = int(jwt_payload[\"sub\"])\n    print(f\"[test_user] email={email}, jwt_sub={user_id}\")\n\n    return {\"token\": token, \"user_id\": user_id}\n\n\nclass TestUserBlocked:\n    \"\"\"일반 USER는 admin API 접근 불가\"\"\"\n\n    def test_user_cannot_access_dashboard(self, admin_base, test_user):\n        headers = {\"Authorization\": f\"Bearer {test_user['token']}\"}\n        resp = requests.get(f\"{admin_base}/v1/dashboard\", headers=headers)\n        assert resp.status_code == 403, f\"USER가 dashboard 접근 가능. status={resp.status_code}\"\n\n\nclass TestAdminAccess:\n    \"\"\"ADMIN은 읽기/쓰기 가능, 역할 변경 불가\"\"\"\n\n    def test_admin_can_read_dashboard(self, admin_base, test_user):\n        user_id = test_user[\"user_id\"]\n        mysql_exec(f\"UPDATE tbl_user SET account_role='ADMIN' WHERE user_id={user_id}\")\n        try:\n            headers = {\"Authorization\": f\"Bearer {test_user['token']}\"}\n            resp = requests.get(f\"{admin_base}/v1/dashboard\", headers=headers)\n            assert_status(resp, 200)\n        finally:\n            mysql_exec(f\"UPDATE tbl_user SET account_role='USER' WHERE user_id={user_id}\")\n\n    def test_admin_can_read_users(self, admin_base, test_user):\n        user_id = test_user[\"user_id\"]\n        mysql_exec(f\"UPDATE tbl_user SET account_role='ADMIN' WHERE user_id={user_id}\")\n        try:\n            headers = {\"Authorization\": f\"Bearer {test_user['token']}\"}\n            resp = requests.get(f\"{admin_base}/v1/users\", headers=headers)\n            assert_status(resp, 200)\n        finally:\n            mysql_exec(f\"UPDATE tbl_user SET account_role='USER' WHERE user_id={user_id}\")\n\n    def test_admin_cannot_change_user_role(self, admin_base, test_user):\n        user_id = test_user[\"user_id\"]\n        mysql_exec(f\"UPDATE tbl_user SET account_role='ADMIN' WHERE user_id={user_id}\")\n        try:\n            headers = {\"Authorization\": f\"Bearer {test_user['token']}\"}\n            resp = requests.patch(\n                f\"{admin_base}/v1/users/{user_id}/role\",\n                json={\"role\": \"SUPER_ADMIN\"},\n                headers=headers,\n            )\n            assert resp.status_code == 403, f\"ADMIN이 역할 변경 가능. status={resp.status_code}\"\n        finally:\n            mysql_exec(f\"UPDATE tbl_user SET account_role='USER' WHERE user_id={user_id}\")\n\n\nclass TestSuperAdminFullAccess:\n    \"\"\"SUPER_ADMIN은 모든 API 접근 가능\"\"\"\n\n    def test_super_admin_full_access(self, admin_base, test_user):\n        user_id = test_user[\"user_id\"]\n        mysql_exec(f\"UPDATE tbl_user SET account_role='SUPER_ADMIN' WHERE user_id={user_id}\")\n        try:\n            headers = {\"Authorization\": f\"Bearer {test_user['token']}\"}\n\n            # 읽기\n            resp = requests.get(f\"{admin_base}/v1/dashboard\", headers=headers)\n            assert_status(resp, 200)\n\n            # 유저 목록\n            resp = requests.get(f\"{admin_base}/v1/users\", headers=headers)\n            assert_status(resp, 200)\n        finally:\n            mysql_exec(f\"UPDATE tbl_user SET account_role='USER' WHERE user_id={user_id}\")\n"
  },
  {
    "path": "e2e-tests/test_35_user_nickname.py",
    "content": "\"\"\"\n유저 닉네임(이름) 변경 E2E 테스트.\n일반 유저의 PATCH /api/v1/users/me/name 및\n어드민의 PATCH /internal-api/v1/users/{id}/name 엔드포인트를 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\ndef test_change_my_name(base_url, auth_headers):\n    \"\"\"내 닉네임을 변경하고 me 조회로 반영을 확인합니다.\"\"\"\n    url = f\"{base_url}/v1/users/me/name\"\n    payload = {\"name\": \"변경된이름\"}\n    print(f\"\\n[test_change_my_name] PATCH {url}\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_change_my_name] status={resp.status_code}, body={resp.text[:400]}\")\n    assert_status(resp, 200)\n\n    # me 조회로 변경 확인\n    me_url = f\"{base_url}/v1/users/me\"\n    me_resp = requests.get(me_url, headers=auth_headers)\n    assert_status(me_resp, 200)\n    me_data = get_data(me_resp)\n    assert me_data.get(\"userName\") == \"변경된이름\", f\"이름이 변경되지 않았습니다: {me_data}\"\n    print(f\"[test_change_my_name] 이름 변경 확인 완료: {me_data.get('userName')}\")\n\n    # 원래 이름으로 복원\n    restore_payload = {\"name\": \"E2E테스터\"}\n    requests.patch(url, json=restore_payload, headers=auth_headers)\n\n\ndef test_change_name_blank_returns_400(base_url, auth_headers):\n    \"\"\"빈 이름으로 변경 시 400을 반환합니다.\"\"\"\n    url = f\"{base_url}/v1/users/me/name\"\n    payload = {\"name\": \"\"}\n    print(f\"\\n[test_change_name_blank] PATCH {url}\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_change_name_blank] status={resp.status_code}, body={resp.text[:400]}\")\n    assert resp.status_code == 400, f\"빈 이름에 400을 기대했으나 {resp.status_code}: {resp.text}\"\n\n\ndef test_change_name_too_long_returns_400(base_url, auth_headers):\n    \"\"\"8자 이름으로 변경 시 400을 반환합니다.\"\"\"\n    url = f\"{base_url}/v1/users/me/name\"\n    payload = {\"name\": \"가\" * 8}\n    print(f\"\\n[test_change_name_too_long] PATCH {url}\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_change_name_too_long] status={resp.status_code}, body={resp.text[:400]}\")\n    assert resp.status_code == 400, f\"8자 이름에 400을 기대했으나 {resp.status_code}: {resp.text}\"\n\n\ndef test_change_name_too_short_returns_400(base_url, auth_headers):\n    \"\"\"1자 이름으로 변경 시 400을 반환합니다.\"\"\"\n    url = f\"{base_url}/v1/users/me/name\"\n    payload = {\"name\": \"가\"}\n    print(f\"\\n[test_change_name_too_short] PATCH {url}\")\n    resp = requests.patch(url, json=payload, headers=auth_headers)\n    print(f\"[test_change_name_too_short] status={resp.status_code}, body={resp.text[:400]}\")\n    assert resp.status_code == 400, f\"1자 이름에 400을 기대했으나 {resp.status_code}: {resp.text}\"\n"
  },
  {
    "path": "e2e-tests/test_36_host_master_transfer.py",
    "content": "\"\"\"\n호스트 마스터 권한 양도 E2E 테스트.\n호스트 생성 -> 멤버 초대/가입 -> 마스터 양도 -> 검증 흐름을 테스트합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\n@pytest.fixture(scope=\"module\")\ndef transfer_state():\n    \"\"\"마스터 양도 테스트 전용 상태\"\"\"\n    class TransferState:\n        host_id: int = 0\n        master_token: str = \"\"\n        member_token: str = \"\"\n        master_user_id: int = 0\n        member_user_id: int = 0\n    return TransferState()\n\n\ndef _login(base_url, email, name):\n    \"\"\"로컬 로그인 후 (token, userId) 반환\"\"\"\n    url = f\"{base_url}/v1/auth/oauth/local/login\"\n    payload = {\n        \"email\": email,\n        \"name\": name,\n        \"phoneNumber\": \"010-0000-0000\",\n        \"profileImage\": None,\n        \"marketingAgree\": False,\n    }\n    resp = requests.post(url, json=payload)\n    assert_status(resp, 200)\n    data = get_data(resp)\n    return data[\"accessToken\"], data.get(\"userId\")\n\n\ndef test_setup_master_and_member(base_url, transfer_state):\n    \"\"\"마스터와 멤버 유저를 로그인합니다.\"\"\"\n    master_token, _ = _login(base_url, \"transfer-master@dudoong.com\", \"양도마스터\")\n    member_token, _ = _login(base_url, \"transfer-member@dudoong.com\", \"양도멤버\")\n    transfer_state.master_token = master_token\n    transfer_state.member_token = member_token\n\n    # 각 유저 정보 조회\n    for label, token, attr in [\n        (\"master\", master_token, \"master_user_id\"),\n        (\"member\", member_token, \"member_user_id\"),\n    ]:\n        resp = requests.get(\n            f\"{base_url}/v1/users/me\",\n            headers={\"Authorization\": f\"Bearer {token}\"},\n        )\n        if resp.status_code == 200:\n            data = get_data(resp)\n            user_id = data.get(\"userId\") or data.get(\"id\")\n            setattr(transfer_state, attr, user_id)\n            print(f\"[setup] {label} userId={user_id}\")\n\n\ndef test_create_host_for_transfer(base_url, transfer_state):\n    \"\"\"마스터가 호스트를 생성합니다.\"\"\"\n    if not transfer_state.master_token:\n        pytest.skip(\"마스터 토큰 없음\")\n\n    url = f\"{base_url}/v1/hosts\"\n    payload = {\n        \"name\": \"양도테스트호스트\",\n        \"contactEmail\": \"transfer@dudoong.com\",\n        \"contactNumber\": \"010-1234-5678\",\n    }\n    headers = {\"Authorization\": f\"Bearer {transfer_state.master_token}\"}\n    resp = requests.post(url, json=payload, headers=headers)\n    print(f\"[create_host] status={resp.status_code}, body={resp.text[:400]}\")\n    assert_status(resp, 200)\n    data = get_data(resp)\n    transfer_state.host_id = data.get(\"hostId\") or data.get(\"id\")\n    print(f\"[create_host] hostId={transfer_state.host_id}\")\n\n\ndef test_invite_member(base_url, transfer_state):\n    \"\"\"마스터가 멤버를 초대합니다.\"\"\"\n    if not transfer_state.host_id or not transfer_state.member_user_id:\n        pytest.skip(\"호스트 또는 멤버 정보 없음\")\n\n    url = f\"{base_url}/v1/hosts/{transfer_state.host_id}/invite\"\n    payload = {\"email\": \"transfer-member@dudoong.com\", \"role\": \"MANAGER\"}\n    headers = {\"Authorization\": f\"Bearer {transfer_state.master_token}\"}\n    resp = requests.post(url, json=payload, headers=headers)\n    print(f\"[invite] status={resp.status_code}, body={resp.text[:400]}\")\n    # 초대 방식이 다를 수 있으므로 200 또는 201 허용\n    assert resp.status_code in (200, 201), f\"초대 실패: {resp.text[:300]}\"\n\n\ndef test_member_join_host(base_url, transfer_state):\n    \"\"\"멤버가 호스트에 가입합니다.\"\"\"\n    if not transfer_state.host_id:\n        pytest.skip(\"호스트 정보 없음\")\n\n    url = f\"{base_url}/v1/hosts/{transfer_state.host_id}/join\"\n    headers = {\"Authorization\": f\"Bearer {transfer_state.member_token}\"}\n    resp = requests.post(url, headers=headers)\n    print(f\"[join] status={resp.status_code}, body={resp.text[:400]}\")\n    assert_status(resp, 200)\n\n\ndef test_transfer_master(base_url, transfer_state):\n    \"\"\"마스터가 멤버에게 권한을 양도합니다.\"\"\"\n    if not transfer_state.host_id or not transfer_state.member_user_id:\n        pytest.skip(\"호스트 또는 멤버 정보 없음\")\n\n    url = f\"{base_url}/v1/hosts/{transfer_state.host_id}/transfer-master\"\n    payload = {\"newMasterUserId\": transfer_state.member_user_id}\n    headers = {\"Authorization\": f\"Bearer {transfer_state.master_token}\"}\n    resp = requests.post(url, json=payload, headers=headers)\n    print(f\"[transfer] status={resp.status_code}, body={resp.text[:400]}\")\n    assert_status(resp, 200)\n    data = get_data(resp)\n    print(f\"[transfer] 양도 완료, masterUserId={data.get('masterUserId')}\")\n\n\ndef test_verify_new_master(base_url, transfer_state):\n    \"\"\"양도 후 호스트 상세 조회로 새 마스터를 검증합니다.\"\"\"\n    if not transfer_state.host_id:\n        pytest.skip(\"호스트 정보 없음\")\n\n    url = f\"{base_url}/v1/hosts/{transfer_state.host_id}\"\n    headers = {\"Authorization\": f\"Bearer {transfer_state.member_token}\"}\n    resp = requests.get(url, headers=headers)\n    print(f\"[verify] status={resp.status_code}, body={resp.text[:400]}\")\n    assert_status(resp, 200)\n    data = get_data(resp)\n\n    # hostUsers 목록에서 마스터 역할 확인\n    host_users = data.get(\"hostUsers\", [])\n    master_users = [hu for hu in host_users if hu.get(\"role\") == \"마스터\" or hu.get(\"role\") == \"MASTER\"]\n    if master_users:\n        new_master_id = master_users[0].get(\"userId\")\n        assert new_master_id == transfer_state.member_user_id, (\n            f\"새 마스터 userId={new_master_id}, 기대값={transfer_state.member_user_id}\"\n        )\n        print(f\"[verify] 새 마스터 확인 완료: userId={new_master_id}\")\n    else:\n        print(f\"[verify] hostUsers에서 마스터 역할을 찾을 수 없습니다: {host_users}\")\n\n\ndef test_old_master_cannot_transfer_again(base_url, transfer_state):\n    \"\"\"양도 후 기존 마스터가 다시 양도를 시도하면 실패합니다.\"\"\"\n    if not transfer_state.host_id or not transfer_state.master_user_id:\n        pytest.skip(\"호스트 또는 마스터 정보 없음\")\n\n    url = f\"{base_url}/v1/hosts/{transfer_state.host_id}/transfer-master\"\n    payload = {\"newMasterUserId\": transfer_state.master_user_id}\n    headers = {\"Authorization\": f\"Bearer {transfer_state.master_token}\"}\n    resp = requests.post(url, json=payload, headers=headers)\n    print(f\"[old_master_retry] status={resp.status_code}, body={resp.text[:300]}\")\n    # 마스터가 아니므로 403 또는 400 기대\n    assert resp.status_code in (400, 403), (\n        f\"기존 마스터의 재양도 시도가 차단되지 않음: status={resp.status_code}\"\n    )\n    print(\"[old_master_retry] 기존 마스터의 재양도 시도가 정상적으로 차단됨\")\n"
  },
  {
    "path": "e2e-tests/test_37_order_cancel_reason.py",
    "content": "\"\"\"\n주문 취소/환불 사유 저장 및 어드민 환불 상태 변경 E2E 테스트.\n주문 생성 -> 호스트 취소(사유 포함) -> 어드민 환불 완료 흐름을 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\n_state: dict = {\n    \"event_id\": 0,\n    \"ticket_item_id\": 0,\n    \"order_uuid\": \"\",\n    \"buyer_headers\": {},\n    \"admin_headers\": {},\n    \"admin_base_url\": \"\",\n}\n\n\ndef _login(base_url: str, email: str, name: str) -> dict:\n    resp = requests.post(\n        f\"{base_url}/v1/auth/oauth/local/login\",\n        json={\n            \"email\": email,\n            \"name\": name,\n            \"phoneNumber\": \"010-0000-0000\",\n            \"profileImage\": None,\n            \"marketingAgree\": False,\n        },\n    )\n    assert resp.status_code == 200, f\"로그인 실패: {resp.text}\"\n    return get_data(resp)\n\n\ndef _create_order(base_url: str, ticket_item_id: int, headers: dict) -> str:\n    \"\"\"장바구니 생성 + 주문 생성 후 order_uuid 반환.\"\"\"\n    cart_resp = requests.post(\n        f\"{base_url}/v1/carts\",\n        json={\"items\": [{\"itemId\": ticket_item_id, \"quantity\": 1, \"options\": []}]},\n        headers=headers,\n    )\n    assert cart_resp.status_code in (200, 201), f\"장바구니 실패: {cart_resp.text[:200]}\"\n    cart_id = get_data(cart_resp)[\"cartId\"]\n\n    order_resp = requests.post(\n        f\"{base_url}/v1/orders/\",\n        json={\"couponId\": None, \"cartId\": cart_id},\n        headers=headers,\n    )\n    assert order_resp.status_code in (200, 201), f\"주문 실패: {order_resp.text[:200]}\"\n    return get_data(order_resp)[\"orderId\"]\n\n\ndef test_setup_refund_reason_scenario(base_url, auth_headers, state):\n    \"\"\"환불 사유 테스트를 위한 이벤트 셋업.\"\"\"\n    if not state.host_id:\n        pytest.skip(\"host_id가 없어 테스트를 건너뜁니다.\")\n\n    _state[\"admin_base_url\"] = base_url.replace(\"/api\", \"/internal-api\")\n    _state[\"admin_headers\"] = auth_headers\n\n    # admin 유저를 ADMIN으로 승격 (internal-api 접근을 위해)\n    import subprocess, base64, json as _json\n    token = auth_headers[\"Authorization\"].replace(\"Bearer \", \"\")\n    p = token.split(\".\")[1]\n    p += \"=\" * (4 - len(p) % 4)\n    uid = int(_json.loads(base64.b64decode(p))[\"sub\"])\n    subprocess.run([\"mysql\", \"-h\", \"127.0.0.1\", \"-P\", \"13306\", \"-u\", \"dudoong\", \"-pdudoong\", \"dudoong\", \"-e\", f\"UPDATE tbl_user SET account_role='ADMIN' WHERE user_id={uid}\"], capture_output=True)\n    _state[\"admin_user_id\"] = uid\n\n    from datetime import datetime, timedelta\n    future = (datetime.now() + timedelta(days=180)).strftime(\"%Y.%m.%d %H:%M\")\n\n    # 이벤트 생성\n    event_resp = requests.post(\n        f\"{base_url}/v1/events\",\n        json={\"hostId\": state.host_id, \"name\": \"환불사유테스트공연\", \"startAt\": future, \"runTime\": 90},\n        headers=auth_headers,\n    )\n    assert event_resp.status_code in (200, 201), f\"이벤트 생성 실패: {event_resp.text[:200]}\"\n    _state[\"event_id\"] = get_data(event_resp)[\"eventId\"]\n\n    # 기본 정보 설정\n    requests.patch(\n        f\"{base_url}/v1/events/{_state['event_id']}/basic\",\n        json={\"name\": \"환불사유테스트공연\", \"startAt\": future, \"runTime\": 90,\n              \"placeName\": \"환불테스트공연장\", \"placeAddress\": \"서울시 강남구\",\n              \"longitude\": 127.0, \"latitude\": 37.5},\n        headers=auth_headers,\n    )\n\n    # 상세 설정\n    requests.patch(\n        f\"{base_url}/v1/events/{_state['event_id']}/details\",\n        json={\"posterImageKey\": \"test/event/e2e/poster.jpeg\", \"content\": \"환불 사유 테스트 상세\"},\n        headers=auth_headers,\n    )\n\n    # 두둥티켓 생성 (승인 방식)\n    ticket_resp = requests.post(\n        f\"{base_url}/v1/events/{_state['event_id']}/ticketItems\",\n        json={\n            \"payType\": \"두둥티켓\",\n            \"name\": \"환불사유테스트티켓\",\n            \"description\": \"환불 사유 테스트용 두둥티켓\",\n            \"price\": 5000,\n            \"supplyCount\": 20,\n            \"approveType\": \"승인\",\n            \"isQuantityPublic\": True,\n            \"purchaseLimit\": 5,\n            \"bankName\": \"신한\",\n            \"accountNumber\": \"110-123-456789\",\n            \"accountHolder\": \"테스터\",\n        },\n        headers=auth_headers,\n    )\n    assert ticket_resp.status_code in (200, 201), f\"티켓 생성 실패: {ticket_resp.text[:300]}\"\n    _state[\"ticket_item_id\"] = get_data(ticket_resp)[\"ticketItemId\"]\n\n    # 이벤트 오픈\n    open_resp = requests.patch(\n        f\"{base_url}/v1/events/{_state['event_id']}/open\",\n        headers=auth_headers,\n    )\n    assert open_resp.status_code == 200, f\"이벤트 오픈 실패: {open_resp.text[:200]}\"\n\n    # 구매자 로그인\n    buyer_data = _login(base_url, \"refund-reason-buyer@dudoong.com\", \"환불사유구매자\")\n    _state[\"buyer_headers\"] = {\"Authorization\": f\"Bearer {buyer_data['accessToken']}\"}\n    print(f\"[setup] 환불사유 테스트 셋업 완료: event={_state['event_id']}\")\n\n\ndef test_create_orders_for_refund(base_url):\n    \"\"\"환불 테스트용 주문을 생성합니다.\"\"\"\n    if not _state[\"ticket_item_id\"]:\n        pytest.skip(\"셋업 안 됨\")\n\n    _state[\"order_uuid\"] = _create_order(base_url, _state[\"ticket_item_id\"], _state[\"buyer_headers\"])\n    print(f\"[test] 주문 생성: {_state['order_uuid']}\")\n\n\ndef test_admin_complete_refund():\n    \"\"\"어드민이 환불을 완료 처리합니다.\"\"\"\n    order_uuid = _state.get(\"order_uuid\")\n    admin_base = _state.get(\"admin_base_url\")\n    if not order_uuid or not admin_base:\n        pytest.skip(\"셋업 안 됨\")\n\n    url = f\"{admin_base}/v1/orders/{order_uuid}/refund-status\"\n    print(f\"\\n[test_admin_complete] PATCH {url}\")\n    resp = requests.patch(\n        url,\n        json={\"refundStatus\": \"REFUND_COMPLETED\"},\n        headers=_state[\"admin_headers\"],\n    )\n    print(f\"[test_admin_complete] status={resp.status_code}, body={resp.text[:400]}\")\n    assert resp.status_code == 200, f\"환불 완료 처리 실패: {resp.text[:300]}\"\n\n    data = get_data(resp)\n    assert data.get(\"refundStatus\") == \"REFUND_COMPLETED\", f\"환불 상태 불일치: {data.get('refundStatus')}\"\n    print(\"[test_admin_complete] 환불 완료 처리 성공\")\n\n\ndef test_admin_cancel_with_reason(base_url):\n    \"\"\"어드민이 사유를 포함하여 주문을 취소합니다.\"\"\"\n    admin_base = _state.get(\"admin_base_url\")\n    if not _state[\"ticket_item_id\"]:\n        pytest.skip(\"셋업 안 됨\")\n\n    # 취소 테스트용 새 주문 생성\n    order_uuid = _create_order(base_url, _state[\"ticket_item_id\"], _state[\"buyer_headers\"])\n\n    url = f\"{admin_base}/v1/orders/{order_uuid}/cancel\"\n    print(f\"\\n[test_admin_cancel] POST {url}\")\n    resp = requests.post(\n        url,\n        json={\"reason\": \"관리자 판단에 의한 취소\"},\n        headers=_state[\"admin_headers\"],\n    )\n    print(f\"[test_admin_cancel] status={resp.status_code}, body={resp.text[:400]}\")\n    # cancel은 주문 상태에 따라 실패할 수 있으므로 상태코드만 로깅\n    if resp.status_code == 200:\n        data = get_data(resp)\n        assert data.get(\"cancelReason\") == \"관리자 판단에 의한 취소\"\n        print(\"[test_admin_cancel] 사유 포함 취소 성공\")\n    else:\n        print(f\"[test_admin_cancel] 취소 불가 (이미 다른 상태): {resp.status_code}\")\n"
  },
  {
    "path": "e2e-tests/test_38_refund_api.py",
    "content": "\"\"\"\n환불 전용 API E2E 테스트.\n호스트용 환불 조회/확인 API와 어드민용 환불 API를 검증합니다.\n\"\"\"\nimport pytest\nimport requests\n\nfrom conftest import assert_status, get_data\n\n\ndef _create_and_refund_order(base_url, auth_headers, state) -> str:\n    \"\"\"\n    환불 API 테스트 전용: 주문 생성 -> 무료 결제 -> 환불 요청 후 order_uuid 반환.\n    \"\"\"\n    assert state.ticket_item_id, \"ticket_item_id가 없습니다. test_04를 먼저 실행하세요.\"\n\n    # 1) 장바구니 생성\n    cart_resp = requests.post(\n        f\"{base_url}/v1/carts\",\n        json={\"items\": [{\"itemId\": state.ticket_item_id, \"quantity\": 1, \"options\": []}]},\n        headers=auth_headers,\n    )\n    assert cart_resp.status_code in (200, 201), f\"장바구니 생성 실패: {cart_resp.text[:200]}\"\n    cart_id = get_data(cart_resp)[\"cartId\"]\n\n    # 2) 주문 생성\n    order_resp = requests.post(\n        f\"{base_url}/v1/orders/\",\n        json={\"couponId\": None, \"cartId\": cart_id},\n        headers=auth_headers,\n    )\n    assert order_resp.status_code in (200, 201), f\"주문 생성 실패: {order_resp.text[:200]}\"\n    order_uuid = get_data(order_resp)[\"orderId\"]\n\n    # 3) 무료 결제 완료\n    free_resp = requests.post(\n        f\"{base_url}/v1/orders/{order_uuid}/free\",\n        headers=auth_headers,\n    )\n    assert free_resp.status_code == 200, f\"무료 결제 실패: {free_resp.text[:200]}\"\n\n    # 4) 환불 요청\n    refund_resp = requests.post(\n        f\"{base_url}/v1/orders/{order_uuid}/refund\",\n        headers=auth_headers,\n    )\n    assert refund_resp.status_code == 200, f\"환불 요청 실패: {refund_resp.text[:200]}\"\n\n    return order_uuid\n\n\n# ==================== 호스트용 환불 API ====================\n\n\nclass TestHostRefundApi:\n    \"\"\"호스트용 환불 API (/api/v1/events/{eventId}/refunds) 테스트\"\"\"\n\n    @pytest.fixture(autouse=True)\n    def setup(self, base_url, auth_headers, state):\n        self.base_url = base_url\n        self.auth_headers = auth_headers\n        self.state = state\n\n    def test_refund_list(self):\n        \"\"\"환불 목록 조회 API가 정상 응답한다.\"\"\"\n        assert self.state.event_id, \"event_id가 없습니다.\"\n\n        # 환불 대상 주문 생성\n        order_uuid = _create_and_refund_order(\n            self.base_url, self.auth_headers, self.state\n        )\n        self.state.refund_api_order_uuid = order_uuid\n\n        url = f\"{self.base_url}/v1/events/{self.state.event_id}/refunds\"\n        print(f\"\\n[test_refund_list] GET {url}\")\n        resp = requests.get(url, headers=self.auth_headers)\n        print(f\"[test_refund_list] status={resp.status_code}, body={resp.text[:400]}\")\n\n        assert_status(resp, 200)\n        data = get_data(resp)\n        assert \"content\" in data, f\"응답에 content 필드 없음: {data}\"\n        assert len(data[\"content\"]) > 0, \"환불 목록이 비어있습니다.\"\n\n    def test_refund_list_with_status_filter(self):\n        \"\"\"환불 목록 조회 시 refundStatus 필터가 동작한다.\"\"\"\n        url = f\"{self.base_url}/v1/events/{self.state.event_id}/refunds\"\n        resp = requests.get(\n            url,\n            params={\"refundStatus\": \"REFUND_REQUESTED\"},\n            headers=self.auth_headers,\n        )\n        assert_status(resp, 200)\n        data = get_data(resp)\n        for item in data.get(\"content\", []):\n            assert item[\"refundStatus\"] == \"REFUND_REQUESTED\"\n\n    def test_refund_detail(self):\n        \"\"\"환불 상세 조회 API가 정상 응답한다.\"\"\"\n        order_uuid = getattr(self.state, \"refund_api_order_uuid\", None)\n        if not order_uuid:\n            pytest.skip(\"환불 주문이 없습니다.\")\n\n        url = f\"{self.base_url}/v1/events/{self.state.event_id}/refunds/{order_uuid}\"\n        print(f\"\\n[test_refund_detail] GET {url}\")\n        resp = requests.get(url, headers=self.auth_headers)\n        print(f\"[test_refund_detail] status={resp.status_code}, body={resp.text[:400]}\")\n\n        assert_status(resp, 200)\n        data = get_data(resp)\n        assert data[\"orderId\"] == order_uuid\n        assert data[\"refundStatus\"] == \"REFUND_REQUESTED\"\n\n    def test_complete_refund(self):\n        \"\"\"환불 확인 API가 정상 동작한다.\"\"\"\n        order_uuid = getattr(self.state, \"refund_api_order_uuid\", None)\n        if not order_uuid:\n            pytest.skip(\"환불 주문이 없습니다.\")\n\n        url = f\"{self.base_url}/v1/events/{self.state.event_id}/refunds/{order_uuid}/complete\"\n        print(f\"\\n[test_complete_refund] PATCH {url}\")\n        resp = requests.patch(url, headers=self.auth_headers)\n        print(f\"[test_complete_refund] status={resp.status_code}, body={resp.text[:400]}\")\n\n        assert_status(resp, 200)\n        data = get_data(resp)\n        assert data[\"orderId\"] == order_uuid\n        assert data[\"refundStatus\"] == \"REFUND_COMPLETED\"\n\n\n# ==================== 어드민용 환불 API ====================\n\n\nclass TestAdminRefundApi:\n    \"\"\"어드민용 환불 API (/internal-api/v1/refunds) 테스트\"\"\"\n\n    @pytest.fixture(autouse=True)\n    def setup(self, base_url, auth_headers, state):\n        self.base_url = base_url\n        self.auth_headers = auth_headers\n        self.state = state\n        # internal-api base url\n        self.admin_base = self.base_url.replace(\"/api\", \"\")\n\n    def test_admin_refund_list(self):\n        \"\"\"어드민 환불 목록 조회가 정상 응답한다.\"\"\"\n        url = f\"{self.admin_base}/internal-api/v1/refunds\"\n        print(f\"\\n[test_admin_refund_list] GET {url}\")\n        resp = requests.get(url, headers=self.auth_headers)\n        print(f\"[test_admin_refund_list] status={resp.status_code}, body={resp.text[:400]}\")\n\n        # ADMIN 권한이 없으면 403이 정상\n        if resp.status_code == 403:\n            pytest.skip(\"ADMIN 권한이 없어 스킵합니다.\")\n\n        assert_status(resp, 200)\n        data = get_data(resp)\n        assert \"content\" in data, f\"응답에 content 필드 없음: {data}\"\n\n    def test_admin_refund_detail(self):\n        \"\"\"어드민 환불 상세 조회가 정상 응답한다.\"\"\"\n        order_uuid = getattr(self.state, \"refund_api_order_uuid\", None)\n        if not order_uuid:\n            pytest.skip(\"환불 주문이 없습니다.\")\n\n        url = f\"{self.admin_base}/internal-api/v1/refunds/{order_uuid}\"\n        resp = requests.get(url, headers=self.auth_headers)\n\n        if resp.status_code == 403:\n            pytest.skip(\"ADMIN 권한이 없어 스킵합니다.\")\n\n        assert_status(resp, 200)\n        data = get_data(resp)\n        assert data[\"orderId\"] == order_uuid\n        assert \"userId\" in data, \"어드민 응답에 userId 필드가 없습니다.\"\n\n    def test_admin_complete_refund(self):\n        \"\"\"어드민 환불 확인 API가 정상 동작한다.\"\"\"\n        # 새로운 환불 대상 주문 생성\n        order_uuid = _create_and_refund_order(\n            self.base_url, self.auth_headers, self.state\n        )\n\n        url = f\"{self.admin_base}/internal-api/v1/refunds/{order_uuid}/complete\"\n        print(f\"\\n[test_admin_complete_refund] PATCH {url}\")\n        resp = requests.patch(url, headers=self.auth_headers)\n        print(f\"[test_admin_complete_refund] status={resp.status_code}, body={resp.text[:400]}\")\n\n        if resp.status_code == 403:\n            pytest.skip(\"ADMIN 권한이 없어 스킵합니다.\")\n\n        assert_status(resp, 200)\n        data = get_data(resp)\n        assert data[\"orderId\"] == order_uuid\n        assert data[\"refundStatus\"] == \"REFUND_COMPLETED\"\n        assert \"userId\" in data\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.5-bin.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\nAPP_HOME=$( cd \"${APP_HOME:-./}\" && pwd -P ) || exit\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=${0##*/}\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n# Collect all arguments for the java command;\n#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of\n#     shell script including quotes and variable substitutions, so put them in\n#     double quotes to make sure that they get re-expanded; and\n#   * put everything else in single quotes, so that it's not re-expanded.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        org.gradle.wrapper.GradleWrapperMain \\\n        \"$@\"\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\n@rem you may not use this file except in compliance with the License.\n@rem You may obtain a copy of the License at\n@rem\n@rem      https://www.apache.org/licenses/LICENSE-2.0\n@rem\n@rem Unless required by applicable law or agreed to in writing, software\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n@rem See the License for the specific language governing permissions and\n@rem limitations under the License.\n@rem\n\n@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto execute\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto execute\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "lombok.config",
    "content": "lombok.addLombokGeneratedAnnotation = true\nlombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier\n"
  },
  {
    "path": "scripts/local-start.sh",
    "content": "#!/bin/bash\nset -e\n\necho \"=== DuDoong Local Dev Environment ===\"\n\necho \"[1/3] Starting Docker containers (MySQL + Redis)...\"\ndocker-compose up -d\n\necho \"[2/3] Waiting for MySQL to be ready...\"\nfor i in $(seq 1 30); do\n    if docker-compose exec -T mysql mysqladmin ping -h localhost -u dudoong -pdudoong --silent 2>/dev/null; then\n        echo \"MySQL is ready.\"\n        break\n    fi\n    echo \"Waiting... ($i/30)\"\n    sleep 2\ndone\n\necho \"[3/3] Starting DuDoong API with 'local' profile...\"\necho \"Swagger UI: http://localhost:8080/api/swagger-ui/index.html\"\n./gradlew :DuDoong-Api:bootRun --args='--spring.profiles.active=local'\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "plugins {\n    id(\"org.gradle.toolchains.foojay-resolver-convention\") version \"0.7.0\"\n}\n\nrootProject.name = \"DuDoong\"\ninclude(\"DuDoong-Domain\")\ninclude(\"DuDoong-Infrastructure\")\ninclude(\"DuDoong-Admin\")\ninclude(\"DuDoong-Api\")\ninclude(\"DuDoong-Common\")\ninclude(\"DuDoong-Batch\")\n"
  }
]