diff --git a/.actrc b/.actrc deleted file mode 100644 index f277761d1..000000000 --- a/.actrc +++ /dev/null @@ -1,3 +0,0 @@ ---container-architecture linux/arm64 --P ubuntu-24.04-arm=catthehacker/ubuntu:runner-latest ---container-options -u=root diff --git a/.agents/skills/deploy-batch/SKILL.md b/.agents/skills/deploy-batch/SKILL.md deleted file mode 100644 index 93d3ac4a5..000000000 --- a/.agents/skills/deploy-batch/SKILL.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -name: deploy-batch -description: | - 배치 모듈 배포. "배치 배포", "batch deploy", "배치 이미지 올려줘" 요청 시 사용. - 단계별 스크립트로 구성되어 개별 테스트 가능. -disable-model-invocation: true -allowed-tools: Bash, Read, Edit, Write, AskUserQuestion ---- - -# Batch 모듈 배포 - -## 현재 상태 - -### 사전 조건 -!`.claude/skills/deploy-batch/scripts/check-prerequisites.sh 2>&1 || true` - -### 배포 버전 기준 -!`.claude/skills/deploy-batch/scripts/check-version.sh 2>&1 || true` - -## 배포 플로우 - -1. 사전 조건을 확인한다. -2. 최신 environment repository `main`의 development/production 태그를 조회한다. -3. 두 SemVer 중 최댓값의 patch 증가값을 기본 버전으로 추천한다. -4. 사용자에게 추천 버전 또는 더 큰 exact `X.Y.Z` 버전을 입력받는다. -5. 선택한 버전을 `check-version.sh `으로 다시 검증한다. -6. 배포 환경과 cosign 서명 여부를 확인한다. -7. 같은 exact version으로 build, push, kustomize update를 순서대로 실행한다. - -## 실행 가이드 - -### 1단계: 사전 조건 확인 - -```bash -.claude/skills/deploy-batch/scripts/check-prerequisites.sh -``` - -실패하면 안내된 조건을 충족한 뒤 처음부터 다시 확인한다. - -### 2단계: 기준 버전과 추천값 확인 - -```bash -.claude/skills/deploy-batch/scripts/check-version.sh -``` - -출력 필드: - -- `PRODUCTION_VERSION`: 최신 production batch 버전 -- `DEVELOPMENT_VERSION`: 최신 development batch 버전 -- `BASE_VERSION`: 두 환경의 SemVer 최댓값 -- `RECOMMENDED_VERSION`: `BASE_VERSION`의 patch 증가값 - -태그 누락, invalid SemVer, fetch 실패 시 임의의 버전을 만들지 말고 중단한다. - -### 3단계: 사용자 선택 - -AskUserQuestion으로 다음 항목을 확인한다. - -- 버전: `RECOMMENDED_VERSION` 사용 또는 기준보다 큰 minor/major exact `X.Y.Z` 직접 입력 -- 환경: Production만, Development만, 둘 다(권장) -- cosign: 서명 포함(권장), 서명 제외 - -선택한 exact version을 이후 단계의 `VERSION`으로 고정하고 검증한다. - -```bash -.claude/skills/deploy-batch/scripts/check-version.sh "$VERSION" -``` - -### 4단계: 이미지 빌드 - -```bash -.claude/skills/deploy-batch/scripts/build-image.sh "$VERSION" -``` - -### 5단계: 이미지 푸시 - -```bash -.claude/skills/deploy-batch/scripts/push-image.sh "$VERSION" --sign -.claude/skills/deploy-batch/scripts/push-image.sh "$VERSION" -``` - -### 6단계: kustomize 업데이트 - -```bash -.claude/skills/deploy-batch/scripts/update-kustomize.sh "$VERSION" both -.claude/skills/deploy-batch/scripts/update-kustomize.sh "$VERSION" production -.claude/skills/deploy-batch/scripts/update-kustomize.sh "$VERSION" development -``` - -## 테스트 - -외부 변경 없이 명령 계약을 확인한다. - -```bash -.claude/skills/deploy-batch/scripts/build-image.sh "$VERSION" --dry-run -.claude/skills/deploy-batch/scripts/push-image.sh "$VERSION" --dry-run --sign -.claude/skills/deploy-batch/scripts/update-kustomize.sh "$VERSION" both --dry-run -.claude/skills/deploy-batch/scripts/test-version-policy.sh -``` - -## 주의사항 - -- version은 prerelease/build metadata가 없는 exact `X.Y.Z`만 허용한다. -- 선택 버전은 development/production 배포 버전의 최댓값보다 커야 한다. -- 이미지 태그는 `batch_X.Y.Z`이며 기존 registry 태그를 덮어쓰지 않는다. -- kustomize update는 선택한 environment overlay만 변경한다. -- cosign 서명 시 cosign key와 password가 필요하다. diff --git a/.agents/skills/deploy-batch/scripts/build-image.sh b/.agents/skills/deploy-batch/scripts/build-image.sh deleted file mode 100755 index d6e553aca..000000000 --- a/.agents/skills/deploy-batch/scripts/build-image.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# build-image.sh -# Gradle 빌드 + Docker 이미지 빌드 - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' - -# 파라미터 파싱 -VERSION="" -DRY_RUN=false -SKIP_GRADLE=false -SEMVER_PATTERN='^(0|[1-9][0-9]*)\.((0|[1-9][0-9]*))\.((0|[1-9][0-9]*))$' - -while [[ $# -gt 0 ]]; do - case $1 in - --dry-run) - DRY_RUN=true - shift - ;; - --skip-gradle) - SKIP_GRADLE=true - shift - ;; - -*) - echo "Usage: $0 [--dry-run] [--skip-gradle]" >&2 - exit 1 - ;; - *) - [[ -z "$VERSION" ]] || { - echo "Usage: $0 [--dry-run] [--skip-gradle]" >&2 - exit 1 - } - VERSION="$1" - shift - ;; - esac -done - -[[ -n "$VERSION" ]] || { - echo -e "${RED}[ERROR]${NC} exact X.Y.Z 버전이 필요합니다" >&2 - exit 1 -} -[[ "$VERSION" =~ $SEMVER_PATTERN ]] || { - echo -e "${RED}[ERROR]${NC} 버전은 exact X.Y.Z 형식이어야 합니다: $VERSION" >&2 - exit 1 -} - -REGISTRY="docker-registry.bottle-note.com" -IMAGE_NAME="bottlenote-batch" -FULL_TAG="${REGISTRY}/${IMAGE_NAME}:batch_${VERSION}" - -echo "=== 빌드 시작 ===" -echo "버전: $VERSION" -echo "이미지: $FULL_TAG" -echo "" - -# 1. Gradle 빌드 -if [[ "$SKIP_GRADLE" == "false" ]]; then - echo -e "${GREEN}[1/2]${NC} Gradle 빌드..." - if [[ "$DRY_RUN" == "true" ]]; then - echo -e "${YELLOW}[DRY-RUN]${NC} ./gradlew :bottlenote-batch:build -x test --build-cache --parallel" - else - cd "$PROJECT_ROOT" - ./gradlew :bottlenote-batch:build -x test --build-cache --parallel - echo -e "${GREEN}[OK]${NC} Gradle 빌드 완료" - fi -else - echo -e "${YELLOW}[SKIP]${NC} Gradle 빌드 건너뜀" -fi - -# 2. Docker 빌드 -echo -e "${GREEN}[2/2]${NC} Docker 이미지 빌드..." -if [[ "$DRY_RUN" == "true" ]]; then - echo -e "${YELLOW}[DRY-RUN]${NC} docker build --platform linux/arm64 -f Dockerfile-batch -t $FULL_TAG ." -else - cd "$PROJECT_ROOT" - docker build --platform linux/arm64 -f Dockerfile-batch -t "$FULL_TAG" . - echo -e "${GREEN}[OK]${NC} Docker 빌드 완료: $FULL_TAG" -fi - -echo "" -echo "---" -echo "IMAGE=$FULL_TAG" diff --git a/.agents/skills/deploy-batch/scripts/bump-version.sh b/.agents/skills/deploy-batch/scripts/bump-version.sh deleted file mode 100755 index 25e64c43a..000000000 --- a/.agents/skills/deploy-batch/scripts/bump-version.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# 명시적 SemVer를 비파괴적으로 증가시킨다. - -SEMVER_PATTERN='^(0|[1-9][0-9]*)\.((0|[1-9][0-9]*))\.((0|[1-9][0-9]*))$' -BASE_VERSION="" -BUMP_TYPE="patch" -VALUE_ONLY=false - -while [[ $# -gt 0 ]]; do - case $1 in - --patch) - BUMP_TYPE="patch" - shift - ;; - --minor) - BUMP_TYPE="minor" - shift - ;; - --major) - BUMP_TYPE="major" - shift - ;; - --value-only) - VALUE_ONLY=true - shift - ;; - -*) - echo "Usage: $0 [--patch|--minor|--major] [--value-only]" >&2 - exit 1 - ;; - *) - [[ -z "$BASE_VERSION" ]] || { - echo "Usage: $0 [--patch|--minor|--major] [--value-only]" >&2 - exit 1 - } - BASE_VERSION=$1 - shift - ;; - esac -done - -[[ -n "$BASE_VERSION" ]] || { - echo "[ERROR] 기준 버전이 필요합니다" >&2 - exit 1 -} -[[ "$BASE_VERSION" =~ $SEMVER_PATTERN ]] || { - echo "[ERROR] 기준 버전은 exact X.Y.Z 형식이어야 합니다: $BASE_VERSION" >&2 - exit 1 -} - -IFS=. read -r major minor patch <<< "$BASE_VERSION" -case $BUMP_TYPE in - major) - new_version="$((major + 1)).0.0" - ;; - minor) - new_version="${major}.$((minor + 1)).0" - ;; - patch) - new_version="${major}.${minor}.$((patch + 1))" - ;; -esac - -if [[ "$VALUE_ONLY" == "true" ]]; then - echo "$new_version" -else - echo "BASE_VERSION=$BASE_VERSION" - echo "BUMP_TYPE=$BUMP_TYPE" - echo "VERSION=$new_version" -fi diff --git a/.agents/skills/deploy-batch/scripts/check-prerequisites.sh b/.agents/skills/deploy-batch/scripts/check-prerequisites.sh deleted file mode 100755 index 83a71499c..000000000 --- a/.agents/skills/deploy-batch/scripts/check-prerequisites.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# check-prerequisites.sh -# 배포에 필요한 도구와 키 존재 확인 -# 필수 조건 미충족 시 즉시 중단 - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' - -panic() { - echo -e "${RED}[FATAL]${NC} $1" >&2 - echo -e "${YELLOW}[해결방법]${NC} $2" >&2 - exit 1 -} - -ok() { - echo -e "${GREEN}[OK]${NC} $1" -} - -echo "=== 사전 조건 확인 ===" - -# 1. Git 설치 확인 -if command -v git &> /dev/null; then - GIT_VERSION=$(git --version) - ok "git: $GIT_VERSION" -else - panic "git 미설치" \ - "brew install git" -fi - -# 2. environment repository 확인 -ENV_REPO_DIR="$PROJECT_ROOT/git.environment-variables" -if git -C "$ENV_REPO_DIR" rev-parse --is-inside-work-tree &> /dev/null; then - ok "environment repository 초기화됨" -else - panic "environment repository가 초기화되지 않음: $ENV_REPO_DIR" \ - "git submodule update --init --recursive" -fi - -# 3. age 키 확인 -AGE_KEY_PATH="${HOME}/.config/sops/age/keys.txt" -if [[ -f "$AGE_KEY_PATH" ]]; then - ok "age 키: $AGE_KEY_PATH" -else - panic "age 키 없음: $AGE_KEY_PATH" \ - "age-keygen -o ~/.config/sops/age/keys.txt" -fi - -# 4. sops 설치 확인 -if command -v sops &> /dev/null; then - SOPS_VERSION=$(sops --version 2>&1 | head -1) - ok "sops: $SOPS_VERSION" -else - panic "sops 미설치" \ - "brew install sops" -fi - -# 5. docker 설치 확인 -if command -v docker &> /dev/null; then - DOCKER_VERSION=$(docker --version) - ok "docker: $DOCKER_VERSION" -else - panic "docker 미설치" \ - "brew install --cask docker" -fi - -# 6. kustomize 설치 확인 -if command -v kustomize &> /dev/null; then - KUSTOMIZE_VERSION=$(kustomize version 2>&1 | head -1) - ok "kustomize: $KUSTOMIZE_VERSION" -else - panic "kustomize 미설치" \ - "brew install kustomize" -fi - -# 7. cosign 설치 확인 (선택사항 - 경고만) -if command -v cosign &> /dev/null; then - COSIGN_VERSION=$(cosign version 2>&1 | grep -i version | head -1 || echo "installed") - ok "cosign: $COSIGN_VERSION" -else - echo -e "${YELLOW}[WARN]${NC} cosign 미설치 (서명 기능 제한) - brew install cosign" -fi - -# 8. registry.sops.env 파일 확인 -SOPS_ENV_FILE="$PROJECT_ROOT/git.environment-variables/storage/docker-registry/registry.sops.env" -if [[ -f "$SOPS_ENV_FILE" ]]; then - ok "registry.sops.env 존재" -else - panic "registry.sops.env 없음" \ - "git submodule update --init --recursive" -fi - -# 9. Dockerfile-batch 확인 -DOCKERFILE="$PROJECT_ROOT/Dockerfile-batch" -if [[ -f "$DOCKERFILE" ]]; then - ok "Dockerfile-batch 존재" -else - panic "Dockerfile-batch 없음: $DOCKERFILE" \ - "프로젝트 루트에 Dockerfile-batch 파일 필요" -fi - -echo "" -echo -e "${GREEN}=== 모든 사전 조건 충족 ===${NC}" -exit 0 diff --git a/.agents/skills/deploy-batch/scripts/check-version.sh b/.agents/skills/deploy-batch/scripts/check-version.sh deleted file mode 100755 index 729ea2528..000000000 --- a/.agents/skills/deploy-batch/scripts/check-version.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# 최신 배포 태그를 기준으로 다음 버전을 추천하고 exact 후보를 검증한다. - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" -ENV_REPO_DIR="${ENV_REPO_DIR:-$PROJECT_ROOT/git.environment-variables}" -SEMVER_PATTERN='^(0|[1-9][0-9]*)\.((0|[1-9][0-9]*))\.((0|[1-9][0-9]*))$' -CANDIDATE_VERSION="${1:-}" - -fail() { - echo "[ERROR] $1" >&2 - exit 1 -} - -is_semver() { - [[ "$1" =~ $SEMVER_PATTERN ]] -} - -semver_gt() { - local left=$1 - local right=$2 - local left_major left_minor left_patch - local right_major right_minor right_patch - - IFS=. read -r left_major left_minor left_patch <<< "$left" - IFS=. read -r right_major right_minor right_patch <<< "$right" - - ((left_major > right_major)) && return 0 - ((left_major < right_major)) && return 1 - ((left_minor > right_minor)) && return 0 - ((left_minor < right_minor)) && return 1 - ((left_patch > right_patch)) -} - -get_deployed_version() { - local environment=$1 - local overlay="deploy/overlays/${environment}/kustomization.yaml" - local tag - - tag=$( - git -C "$ENV_REPO_DIR" show "origin/main:${overlay}" | - awk ' - /^[[:space:]]*-[[:space:]]*name:[[:space:]]*bottlenote-batch[[:space:]]*$/ { - found = 1 - next - } - found && /^[[:space:]]*newTag:[[:space:]]*/ { - sub(/^[[:space:]]*newTag:[[:space:]]*/, "") - print - exit - } - found && /^[[:space:]]*-[[:space:]]*name:/ { - exit - } - ' - ) || fail "${environment} overlay를 origin/main에서 읽지 못했습니다" - - [[ "$tag" == batch_* ]] || - fail "${environment} batch 태그가 없거나 batch_X.Y.Z 형식이 아닙니다" - - local version=${tag#batch_} - is_semver "$version" || - fail "${environment} batch 태그가 exact SemVer가 아닙니다: $tag" - - printf '%s\n' "$version" -} - -[[ -d "$ENV_REPO_DIR" ]] || - fail "environment repository가 없습니다: $ENV_REPO_DIR" -git -C "$ENV_REPO_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1 || - fail "environment repository가 초기화되지 않았습니다: $ENV_REPO_DIR" -git -C "$ENV_REPO_DIR" fetch --quiet origin main || - fail "environment repository origin/main fetch에 실패했습니다" - -PRODUCTION_VERSION=$(get_deployed_version production) -DEVELOPMENT_VERSION=$(get_deployed_version development) - -BASE_VERSION=$PRODUCTION_VERSION -if semver_gt "$DEVELOPMENT_VERSION" "$PRODUCTION_VERSION"; then - BASE_VERSION=$DEVELOPMENT_VERSION -fi - -RECOMMENDED_VERSION=$( - "$SCRIPT_DIR/bump-version.sh" "$BASE_VERSION" --patch --value-only -) - -if [[ -n "$CANDIDATE_VERSION" ]]; then - is_semver "$CANDIDATE_VERSION" || - fail "입력 버전은 exact X.Y.Z 형식이어야 합니다: $CANDIDATE_VERSION" - semver_gt "$CANDIDATE_VERSION" "$BASE_VERSION" || - fail "입력 버전은 현재 최댓값 $BASE_VERSION 보다 커야 합니다" -fi - -echo "PRODUCTION_VERSION=$PRODUCTION_VERSION" -echo "DEVELOPMENT_VERSION=$DEVELOPMENT_VERSION" -echo "BASE_VERSION=$BASE_VERSION" -echo "RECOMMENDED_VERSION=$RECOMMENDED_VERSION" -if [[ -n "$CANDIDATE_VERSION" ]]; then - echo "CANDIDATE_VERSION=$CANDIDATE_VERSION" -fi -echo "STATUS=OK" diff --git a/.agents/skills/deploy-batch/scripts/decrypt-registry.sh b/.agents/skills/deploy-batch/scripts/decrypt-registry.sh deleted file mode 100755 index 32ecf3157..000000000 --- a/.agents/skills/deploy-batch/scripts/decrypt-registry.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# decrypt-registry.sh -# sops로 레지스트리 인증 정보 복호화 - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" - -SOPS_ENV_FILE="$PROJECT_ROOT/git.environment-variables/storage/docker-registry/registry.sops.env" - -# 옵션 파싱 -EXPORT_MODE=false -while [[ $# -gt 0 ]]; do - case $1 in - --export) - EXPORT_MODE=true - shift - ;; - *) - echo "Usage: $0 [--export]" - echo " --export: export 문 형태로 출력 (source로 사용 가능)" - exit 1 - ;; - esac -done - -# age 키 존재 확인 -AGE_KEY_PATH="${HOME}/.config/sops/age/keys.txt" -if [[ ! -f "$AGE_KEY_PATH" ]]; then - echo "ERROR: age 키 없음: $AGE_KEY_PATH" >&2 - exit 1 -fi - -# sops.env 파일 존재 확인 -if [[ ! -f "$SOPS_ENV_FILE" ]]; then - echo "ERROR: registry.sops.env 없음: $SOPS_ENV_FILE" >&2 - exit 1 -fi - -# sops로 복호화 -DECRYPTED=$(sops -d "$SOPS_ENV_FILE" 2>/dev/null) - -# 파싱 -REGISTRY_ADDRESS=$(echo "$DECRYPTED" | grep "^REGISTRY_ADDRESS=" | cut -d'=' -f2-) -REGISTRY_USERNAME=$(echo "$DECRYPTED" | grep "^REGISTRY_USERNAME=" | cut -d'=' -f2-) -REGISTRY_PASSWORD=$(echo "$DECRYPTED" | grep "^REGISTRY_PASSWORD=" | cut -d'=' -f2-) -COSIGN_PASSWORD=$(echo "$DECRYPTED" | grep "^COSIGN_PASSWORD=" | cut -d'=' -f2-) - -if [[ -z "$REGISTRY_ADDRESS" ]] || [[ -z "$REGISTRY_USERNAME" ]] || [[ -z "$REGISTRY_PASSWORD" ]]; then - echo "ERROR: 복호화 실패 또는 필수 필드 누락" >&2 - exit 1 -fi - -if [[ "$EXPORT_MODE" == "true" ]]; then - # source로 사용 가능한 형태로 출력 - echo "export REGISTRY_ADDRESS=\"$REGISTRY_ADDRESS\"" - echo "export REGISTRY_USERNAME=\"$REGISTRY_USERNAME\"" - echo "export REGISTRY_PASSWORD=\"$REGISTRY_PASSWORD\"" - [[ -n "$COSIGN_PASSWORD" ]] && echo "export COSIGN_PASSWORD=\"$COSIGN_PASSWORD\"" -else - # 일반 출력 - echo "REGISTRY_ADDRESS=$REGISTRY_ADDRESS" - echo "REGISTRY_USERNAME=$REGISTRY_USERNAME" - echo "REGISTRY_PASSWORD=***" - echo "COSIGN_PASSWORD=${COSIGN_PASSWORD:+***}" - echo "" - echo "복호화 성공. 전체 값을 보려면 --export 옵션 사용" -fi diff --git a/.agents/skills/deploy-batch/scripts/push-image.sh b/.agents/skills/deploy-batch/scripts/push-image.sh deleted file mode 100755 index af9b2fb3e..000000000 --- a/.agents/skills/deploy-batch/scripts/push-image.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# push-image.sh -# Docker 푸시 및 선택적 cosign 서명 - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' - -# 파라미터 파싱 -VERSION="" -DRY_RUN=false -SIGN=false -SEMVER_PATTERN='^(0|[1-9][0-9]*)\.((0|[1-9][0-9]*))\.((0|[1-9][0-9]*))$' - -while [[ $# -gt 0 ]]; do - case $1 in - --dry-run) - DRY_RUN=true - shift - ;; - --sign) - SIGN=true - shift - ;; - -*) - echo "Usage: $0 [--dry-run] [--sign]" >&2 - exit 1 - ;; - *) - [[ -z "$VERSION" ]] || { - echo "Usage: $0 [--dry-run] [--sign]" >&2 - exit 1 - } - VERSION="$1" - shift - ;; - esac -done - -[[ -n "$VERSION" ]] || { - echo -e "${RED}[ERROR]${NC} exact X.Y.Z 버전이 필요합니다" >&2 - exit 1 -} -[[ "$VERSION" =~ $SEMVER_PATTERN ]] || { - echo -e "${RED}[ERROR]${NC} 버전은 exact X.Y.Z 형식이어야 합니다: $VERSION" >&2 - exit 1 -} - -REGISTRY="docker-registry.bottle-note.com" -IMAGE_NAME="bottlenote-batch" -FULL_TAG="${REGISTRY}/${IMAGE_NAME}:batch_${VERSION}" - -echo "=== Docker 푸시 ===" -echo "이미지: $FULL_TAG" -echo "서명: $SIGN" -echo "" - -# 레지스트리 인증 정보 복호화 -echo -e "${GREEN}[1/3]${NC} 레지스트리 인증..." -if [[ "$DRY_RUN" == "true" ]]; then - echo -e "${YELLOW}[DRY-RUN]${NC} decrypt-registry.sh --export" - REGISTRY_ADDRESS="docker-registry.bottle-note.com" - REGISTRY_USERNAME="test" - REGISTRY_PASSWORD="test" -else - DECRYPT_OUTPUT=$("$SCRIPT_DIR/decrypt-registry.sh" --export) - eval "$DECRYPT_OUTPUT" - - # Docker 로그인 - echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_ADDRESS" -u "$REGISTRY_USERNAME" --password-stdin - echo -e "${GREEN}[OK]${NC} Docker 로그인 완료" -fi - -# Docker 푸시 -echo -e "${GREEN}[2/3]${NC} Docker 이미지 푸시..." -if [[ "$DRY_RUN" == "true" ]]; then - echo -e "${YELLOW}[DRY-RUN]${NC} docker push $FULL_TAG" -else - docker push "$FULL_TAG" - echo -e "${GREEN}[OK]${NC} 푸시 완료: $FULL_TAG" -fi - -# cosign 서명 (선택) -if [[ "$SIGN" == "true" ]]; then - echo -e "${GREEN}[3/3]${NC} cosign 서명..." - - COSIGN_KEY="$PROJECT_ROOT/git.environment-variables/storage/docker-registry/cosign.key" - - if [[ ! -f "$COSIGN_KEY" ]]; then - echo -e "${RED}[ERROR]${NC} cosign 키 없음: $COSIGN_KEY" >&2 - exit 1 - fi - - if ! command -v cosign &> /dev/null; then - echo -e "${RED}[ERROR]${NC} cosign 미설치" >&2 - exit 1 - fi - - if [[ "$DRY_RUN" == "true" ]]; then - echo -e "${YELLOW}[DRY-RUN]${NC} COSIGN_PASSWORD=*** cosign sign --key $COSIGN_KEY $FULL_TAG" - else - # COSIGN_PASSWORD는 decrypt-registry.sh에서 export됨 - if [[ -z "${COSIGN_PASSWORD:-}" ]]; then - echo -e "${RED}[ERROR]${NC} COSIGN_PASSWORD 없음 (registry.sops.env에 추가 필요)" >&2 - exit 1 - fi - export COSIGN_PASSWORD - cosign sign --key "$COSIGN_KEY" "$FULL_TAG" - echo -e "${GREEN}[OK]${NC} cosign 서명 완료" - fi -else - echo -e "${YELLOW}[SKIP]${NC} cosign 서명 건너뜀 (--sign 옵션 없음)" -fi - -echo "" -echo "---" -echo "PUSHED=$FULL_TAG" -echo "SIGNED=$SIGN" diff --git a/.agents/skills/deploy-batch/scripts/test-version-policy.sh b/.agents/skills/deploy-batch/scripts/test-version-policy.sh deleted file mode 100755 index a80c63b0d..000000000 --- a/.agents/skills/deploy-batch/scripts/test-version-policy.sh +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# 로컬 Git fixture로 배포 버전 정책을 회귀 검증한다. - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" -AGENTS_SKILL="$PROJECT_ROOT/.agents/skills/deploy-batch" -CLAUDE_SKILL="$PROJECT_ROOT/.claude/skills/deploy-batch" -TEMP_DIR=$(mktemp -d) -trap 'rm -rf "$TEMP_DIR"' EXIT - -fail() { - echo "[FAIL] $1" >&2 - exit 1 -} - -assert_output() { - local output=$1 - local expected=$2 - grep -Fqx "$expected" <<< "$output" || - fail "출력 누락: $expected" -} - -expect_failure() { - if "$@" >"$TEMP_DIR/expected-failure.out" 2>&1; then - fail "실패해야 하는 명령이 성공했습니다: $*" - fi -} - -write_overlay() { - local repository=$1 - local environment=$2 - local tag=$3 - local overlay_dir="$repository/deploy/overlays/$environment" - - mkdir -p "$overlay_dir" - printf '%s\n' \ - 'images:' \ - '- name: bottlenote-batch' \ - ' newName: docker-registry.bottle-note.com/bottlenote-batch' \ - " newTag: $tag" \ - > "$overlay_dir/kustomization.yaml" -} - -commit_fixture() { - local message=$1 - - git -C "$TEMP_DIR/seed" add deploy - git -C "$TEMP_DIR/seed" commit -q -m "$message" - git -C "$TEMP_DIR/seed" push -q origin main -} - -git init -q --bare "$TEMP_DIR/remote.git" -git init -q -b main "$TEMP_DIR/seed" -git -C "$TEMP_DIR/seed" config user.name "Batch Version Test" -git -C "$TEMP_DIR/seed" config user.email "batch-version-test@example.com" -git -C "$TEMP_DIR/seed" remote add origin "$TEMP_DIR/remote.git" - -write_overlay "$TEMP_DIR/seed" production batch_1.0.6 -write_overlay "$TEMP_DIR/seed" development batch_1.0.6 -commit_fixture "initial tags" -git clone -q "$TEMP_DIR/remote.git" "$TEMP_DIR/environment" - -output=$(ENV_REPO_DIR="$TEMP_DIR/environment" "$SCRIPT_DIR/check-version.sh") -assert_output "$output" "PRODUCTION_VERSION=1.0.6" -assert_output "$output" "DEVELOPMENT_VERSION=1.0.6" -assert_output "$output" "BASE_VERSION=1.0.6" -assert_output "$output" "RECOMMENDED_VERSION=1.0.7" - -write_overlay "$TEMP_DIR/seed" production batch_1.9.9 -write_overlay "$TEMP_DIR/seed" development batch_2.0.0 -commit_fixture "different tags" - -output=$(ENV_REPO_DIR="$TEMP_DIR/environment" "$SCRIPT_DIR/check-version.sh" 2.1.0) -assert_output "$output" "BASE_VERSION=2.0.0" -assert_output "$output" "RECOMMENDED_VERSION=2.0.1" -assert_output "$output" "CANDIDATE_VERSION=2.1.0" -expect_failure env ENV_REPO_DIR="$TEMP_DIR/environment" "$SCRIPT_DIR/check-version.sh" 2.0.0 -expect_failure env ENV_REPO_DIR="$TEMP_DIR/environment" "$SCRIPT_DIR/check-version.sh" 2.0.1-rc.1 - -write_overlay "$TEMP_DIR/seed" production batch_invalid -commit_fixture "invalid tag" -expect_failure env ENV_REPO_DIR="$TEMP_DIR/environment" "$SCRIPT_DIR/check-version.sh" - -write_overlay "$TEMP_DIR/seed" production batch_2.0.0 -write_overlay "$TEMP_DIR/seed" development other_2.0.0 -commit_fixture "missing batch tag" -expect_failure env ENV_REPO_DIR="$TEMP_DIR/environment" "$SCRIPT_DIR/check-version.sh" - -git -C "$TEMP_DIR/environment" remote set-url origin "$TEMP_DIR/missing.git" -expect_failure env ENV_REPO_DIR="$TEMP_DIR/environment" "$SCRIPT_DIR/check-version.sh" - -[[ "$("$SCRIPT_DIR/bump-version.sh" 1.0.6 --patch --value-only)" == "1.0.7" ]] || - fail "patch 증가 실패" -[[ "$("$SCRIPT_DIR/bump-version.sh" 1.0.6 --minor --value-only)" == "1.1.0" ]] || - fail "minor 증가 실패" -[[ "$("$SCRIPT_DIR/bump-version.sh" 1.0.6 --major --value-only)" == "2.0.0" ]] || - fail "major 증가 실패" - -for script in build-image.sh push-image.sh update-kustomize.sh; do - expect_failure "$SCRIPT_DIR/$script" - expect_failure "$SCRIPT_DIR/$script" 1.0.7-rc.1 --dry-run -done -"$SCRIPT_DIR/build-image.sh" 1.0.7 --dry-run --skip-gradle >/dev/null -"$SCRIPT_DIR/push-image.sh" 1.0.7 --dry-run >/dev/null -"$SCRIPT_DIR/update-kustomize.sh" 1.0.7 both --dry-run >/dev/null - -for script in "$AGENTS_SKILL"/scripts/*.sh "$CLAUDE_SKILL"/scripts/*.sh; do - bash -n "$script" -done - -obsolete_pattern='VERSION_''FILE|bottlenote-batch/VER''SION' -if grep -R -E "$obsolete_pattern" "$AGENTS_SKILL" "$CLAUDE_SKILL"; then - fail "VERSION 파일 참조가 남아 있습니다" -fi -version_file="$PROJECT_ROOT/bottlenote-batch/VER""SION" -[[ ! -e "$version_file" ]] || - fail "bottlenote-batch/VER""SION 파일이 남아 있습니다" - -workflow="$PROJECT_ROOT/.github/workflows/deploy_batch.yml" -grep -A3 '^ version:' "$workflow" | grep -q 'required: true' || - fail "workflow required version input이 없습니다" -grep -q 'INPUT_VERSION:.*inputs.version' "$workflow" || - fail "workflow version input 연결이 없습니다" -[[ "$(grep -c 'docker manifest inspect' "$workflow")" -eq 2 ]] || - fail "immutable image 검사는 두 단계에 있어야 합니다" - -find "$AGENTS_SKILL" -type f | sed "s#^$AGENTS_SKILL/##" | sort > "$TEMP_DIR/agents-files" -find "$CLAUDE_SKILL" -type f | sed "s#^$CLAUDE_SKILL/##" | sort > "$TEMP_DIR/claude-files" -cmp -s "$TEMP_DIR/agents-files" "$TEMP_DIR/claude-files" || - fail "deploy-batch 복제본 파일 목록이 다릅니다" - -while IFS= read -r relative_path; do - agents_hash=$(shasum -a 256 "$AGENTS_SKILL/$relative_path" | awk '{print $1}') - claude_hash=$(shasum -a 256 "$CLAUDE_SKILL/$relative_path" | awk '{print $1}') - [[ "$agents_hash" == "$claude_hash" ]] || - fail "deploy-batch 복제본 SHA-256 불일치: $relative_path" -done < "$TEMP_DIR/agents-files" - -echo "PASS: batch version policy" diff --git a/.agents/skills/deploy-batch/scripts/update-kustomize.sh b/.agents/skills/deploy-batch/scripts/update-kustomize.sh deleted file mode 100755 index 16432e88a..000000000 --- a/.agents/skills/deploy-batch/scripts/update-kustomize.sh +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# update-kustomize.sh -# kustomization.yaml 업데이트 및 서브모듈 푸시 - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' - -# 파라미터 파싱 -VERSION="" -ENV="both" -DRY_RUN=false -SEMVER_PATTERN='^(0|[1-9][0-9]*)\.((0|[1-9][0-9]*))\.((0|[1-9][0-9]*))$' - -while [[ $# -gt 0 ]]; do - case $1 in - --dry-run) - DRY_RUN=true - shift - ;; - production|development|both) - ENV="$1" - shift - ;; - -*) - echo "Usage: $0 [production|development|both] [--dry-run]" >&2 - exit 1 - ;; - *) - [[ -z "$VERSION" ]] || { - echo "Usage: $0 [production|development|both] [--dry-run]" >&2 - exit 1 - } - VERSION="$1" - shift - ;; - esac -done - -[[ -n "$VERSION" ]] || { - echo -e "${RED}[ERROR]${NC} exact X.Y.Z 버전이 필요합니다" >&2 - exit 1 -} -[[ "$VERSION" =~ $SEMVER_PATTERN ]] || { - echo -e "${RED}[ERROR]${NC} 버전은 exact X.Y.Z 형식이어야 합니다: $VERSION" >&2 - exit 1 -} - -# 환경 파라미터 검증 -if [[ "$ENV" != "production" ]] && [[ "$ENV" != "development" ]] && [[ "$ENV" != "both" ]]; then - echo -e "${RED}[ERROR]${NC} 잘못된 환경: $ENV (production|development|both)" >&2 - exit 1 -fi - -REGISTRY="docker-registry.bottle-note.com" -IMAGE_NAME="bottlenote-batch" -FULL_TAG="batch_${VERSION}" -SUBMODULE_DIR="$PROJECT_ROOT/git.environment-variables" - -echo "=== Kustomize 업데이트 ===" -echo "버전: $VERSION" -echo "태그: $FULL_TAG" -echo "환경: $ENV" -echo "" - -# 서브모듈 동기화 (최우선) -echo -e "${GREEN}[SYNC]${NC} 서브모듈 원격 동기화..." -if [[ "$DRY_RUN" == "true" ]]; then - echo -e "${YELLOW}[DRY-RUN]${NC} cd $SUBMODULE_DIR && git fetch origin && git reset --hard origin/main" -else - cd "$SUBMODULE_DIR" - - # 현재 브랜치 확인 - CURRENT_BRANCH=$(git branch --show-current) - if [[ "$CURRENT_BRANCH" != "main" ]]; then - echo -e "${YELLOW}[WARN]${NC} 현재 브랜치: $CURRENT_BRANCH (main이 아님)" - git checkout main - fi - - # 원격에서 최신 상태 가져오기 - git fetch origin - - # 로컬 변경사항 확인 및 처리 - if ! git diff --quiet || ! git diff --cached --quiet; then - echo -e "${YELLOW}[WARN]${NC} 로컬 변경사항 있음 - 리셋합니다" - git reset --hard origin/main - else - # 변경사항 없으면 fast-forward - git reset --hard origin/main - fi - - echo -e "${GREEN}[OK]${NC} 서브모듈 동기화 완료 (origin/main)" -fi -echo "" - -update_kustomize() { - local env=$1 - local overlay_dir="$SUBMODULE_DIR/deploy/overlays/$env" - - if [[ ! -d "$overlay_dir" ]]; then - echo -e "${RED}[ERROR]${NC} 디렉토리 없음: $overlay_dir" >&2 - return 1 - fi - - echo -e "${GREEN}[$env]${NC} kustomize 업데이트..." - - if [[ "$DRY_RUN" == "true" ]]; then - echo -e "${YELLOW}[DRY-RUN]${NC} cd $overlay_dir && kustomize edit set image bottlenote-batch=${REGISTRY}/${IMAGE_NAME}:${FULL_TAG}" - else - cd "$overlay_dir" - kustomize edit set image "bottlenote-batch=${REGISTRY}/${IMAGE_NAME}:${FULL_TAG}" - echo -e "${GREEN}[OK]${NC} $env 업데이트 완료" - fi -} - -# 환경별 업데이트 -if [[ "$ENV" == "both" ]] || [[ "$ENV" == "production" ]]; then - update_kustomize "production" -fi - -if [[ "$ENV" == "both" ]] || [[ "$ENV" == "development" ]]; then - update_kustomize "development" -fi - -# 서브모듈 커밋 및 푸시 -echo "" -echo -e "${GREEN}[COMMIT]${NC} 서브모듈 커밋 및 푸시..." - -if [[ "$DRY_RUN" == "true" ]]; then - echo -e "${YELLOW}[DRY-RUN]${NC} cd $SUBMODULE_DIR" - echo -e "${YELLOW}[DRY-RUN]${NC} git add -A" - echo -e "${YELLOW}[DRY-RUN]${NC} git commit -m 'chore(batch): update image tag to ${FULL_TAG}'" - echo -e "${YELLOW}[DRY-RUN]${NC} git push origin main" -else - cd "$SUBMODULE_DIR" - - # 변경사항 확인 - if git diff --quiet && git diff --cached --quiet; then - echo -e "${YELLOW}[SKIP]${NC} 변경사항 없음" - else - git add -A - git commit -m "chore(batch): update image tag to ${FULL_TAG}" - git push origin main - echo -e "${GREEN}[OK]${NC} 서브모듈 푸시 완료" - fi -fi - -echo "" -echo "---" -echo "VERSION=$VERSION" -echo "TAG=$FULL_TAG" -echo "ENVIRONMENT=$ENV" diff --git a/.agents/skills/implement/references/languages/java-spring.md b/.agents/skills/implement/references/languages/java-spring.md index 07036621c..9204c1394 100644 --- a/.agents/skills/implement/references/languages/java-spring.md +++ b/.agents/skills/implement/references/languages/java-spring.md @@ -222,7 +222,7 @@ Pattern: async + `REQUIRES_NEW` separates side-effect transaction from main. Fai | Annotation | Role | Location | Includes | |------------|------|----------|----------| -| `@FacadeService` | Aggregate Root external door | `{domain}/facade/` | `@Service` | +| `@FacadeService` | Aggregate Root external door | impl `Default{Domain}Facade` in `{domain}/service/`, interface in `{domain}/facade/` | `@Service` | | `@DomainRepository` | Pure domain interface | `{domain}/domain/` | (none, marker) | | `@JpaRepositoryImpl` | JPA implementation | `{domain}/repository/` | `@Repository` | | `@DomainEventListener` | Domain event listener | `{domain}/event/` | `@Component` | diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh deleted file mode 100755 index df943d91e..000000000 --- a/.claude/hooks/session-start.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Claude Code 웹 환경에서만 실행 -if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then - exit 0 -fi - -echo "🚀 Starting Docker setup for TestContainers..." - -# Docker가 이미 설치되어 있는지 확인 -if command -v docker &> /dev/null; then - echo "✅ Docker already installed: $(docker --version)" - - # Docker 데몬이 실행 중인지 확인 - if docker info &> /dev/null; then - echo "✅ Docker daemon is already running" - exit 0 - else - echo "🔄 Docker daemon not running, starting..." - fi -else - echo "📦 Installing Docker..." - - # 문제가 되는 PPA 저장소 제거 - rm -f /etc/apt/sources.list.d/deadsnakes-ubuntu-ppa-noble.sources - rm -f /etc/apt/sources.list.d/ondrej-ubuntu-php-noble.sources - - # /tmp 권한 수정 - chmod 1777 /tmp 2>/dev/null || true - - # apt 업데이트 - apt-get update -y -qq - - # Docker GPG 키 및 저장소 추가 - install -m 0755 -d /etc/apt/keyrings - curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc - chmod a+r /etc/apt/keyrings/docker.asc - - echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable" > /etc/apt/sources.list.d/docker.list - - # apt 업데이트 및 Docker 설치 - apt-get update -y -qq - DEBIAN_FRONTEND=noninteractive apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin > /dev/null - - echo "✅ Docker installed successfully: $(docker --version)" -fi - -# Docker 데몬 시작 (vfs storage driver 사용) -if ! docker info &> /dev/null; then - echo "🔄 Starting Docker daemon..." - - # 기존 docker0 인터페이스 정리 (있는 경우) - if [ -d /sys/class/net/docker0 ]; then - echo "🧹 Cleaning up existing docker0 interface..." - ip link delete docker0 2>/dev/null || ifconfig docker0 down 2>/dev/null || true - fi - - # dockerd 프로세스가 이미 실행 중인지 확인 - if pidof dockerd > /dev/null; then - echo "⏳ Docker daemon process found, waiting for it to be ready..." - sleep 3 - else - dockerd --iptables=false --ip-masq=false --storage-driver=vfs > /var/log/dockerd.log 2>&1 & - fi - - # Docker 데몬이 준비될 때까지 대기 - for i in {1..15}; do - if docker info &> /dev/null; then - echo "✅ Docker daemon started successfully" - break - fi - echo "⏳ Waiting for Docker daemon to start... ($i/15)" - sleep 2 - done - - if ! docker info &> /dev/null; then - echo "❌ Failed to start Docker daemon" - echo "📋 Docker logs:" - tail -30 /var/log/dockerd.log - exit 1 - fi -fi - -echo "🎉 Docker setup completed successfully!" -docker info 2>&1 | head -5 || true diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 60c1345ca..000000000 --- a/.claude/settings.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "hooks": { - "SessionStart": [ - { - "hooks": [ - { - "type": "command", - "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh" - } - ] - } - ], - "PostToolUse": [ - { - "matcher": "Edit|Write", - "hooks": [ - { - "type": "command", - "command": "FP=$(cat | jq -r '.tool_input.file_path // empty'); [[ \"$FP\" == *.java || \"$FP\" == *.kt ]] && cd $CLAUDE_PROJECT_DIR && ./gradlew spotlessApply -q 2>/dev/null || true", - "timeout": 30 - } - ] - } - ] - } -} diff --git a/.claude/skills/deploy-batch/SKILL.md b/.claude/skills/deploy-batch/SKILL.md deleted file mode 100644 index 93d3ac4a5..000000000 --- a/.claude/skills/deploy-batch/SKILL.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -name: deploy-batch -description: | - 배치 모듈 배포. "배치 배포", "batch deploy", "배치 이미지 올려줘" 요청 시 사용. - 단계별 스크립트로 구성되어 개별 테스트 가능. -disable-model-invocation: true -allowed-tools: Bash, Read, Edit, Write, AskUserQuestion ---- - -# Batch 모듈 배포 - -## 현재 상태 - -### 사전 조건 -!`.claude/skills/deploy-batch/scripts/check-prerequisites.sh 2>&1 || true` - -### 배포 버전 기준 -!`.claude/skills/deploy-batch/scripts/check-version.sh 2>&1 || true` - -## 배포 플로우 - -1. 사전 조건을 확인한다. -2. 최신 environment repository `main`의 development/production 태그를 조회한다. -3. 두 SemVer 중 최댓값의 patch 증가값을 기본 버전으로 추천한다. -4. 사용자에게 추천 버전 또는 더 큰 exact `X.Y.Z` 버전을 입력받는다. -5. 선택한 버전을 `check-version.sh `으로 다시 검증한다. -6. 배포 환경과 cosign 서명 여부를 확인한다. -7. 같은 exact version으로 build, push, kustomize update를 순서대로 실행한다. - -## 실행 가이드 - -### 1단계: 사전 조건 확인 - -```bash -.claude/skills/deploy-batch/scripts/check-prerequisites.sh -``` - -실패하면 안내된 조건을 충족한 뒤 처음부터 다시 확인한다. - -### 2단계: 기준 버전과 추천값 확인 - -```bash -.claude/skills/deploy-batch/scripts/check-version.sh -``` - -출력 필드: - -- `PRODUCTION_VERSION`: 최신 production batch 버전 -- `DEVELOPMENT_VERSION`: 최신 development batch 버전 -- `BASE_VERSION`: 두 환경의 SemVer 최댓값 -- `RECOMMENDED_VERSION`: `BASE_VERSION`의 patch 증가값 - -태그 누락, invalid SemVer, fetch 실패 시 임의의 버전을 만들지 말고 중단한다. - -### 3단계: 사용자 선택 - -AskUserQuestion으로 다음 항목을 확인한다. - -- 버전: `RECOMMENDED_VERSION` 사용 또는 기준보다 큰 minor/major exact `X.Y.Z` 직접 입력 -- 환경: Production만, Development만, 둘 다(권장) -- cosign: 서명 포함(권장), 서명 제외 - -선택한 exact version을 이후 단계의 `VERSION`으로 고정하고 검증한다. - -```bash -.claude/skills/deploy-batch/scripts/check-version.sh "$VERSION" -``` - -### 4단계: 이미지 빌드 - -```bash -.claude/skills/deploy-batch/scripts/build-image.sh "$VERSION" -``` - -### 5단계: 이미지 푸시 - -```bash -.claude/skills/deploy-batch/scripts/push-image.sh "$VERSION" --sign -.claude/skills/deploy-batch/scripts/push-image.sh "$VERSION" -``` - -### 6단계: kustomize 업데이트 - -```bash -.claude/skills/deploy-batch/scripts/update-kustomize.sh "$VERSION" both -.claude/skills/deploy-batch/scripts/update-kustomize.sh "$VERSION" production -.claude/skills/deploy-batch/scripts/update-kustomize.sh "$VERSION" development -``` - -## 테스트 - -외부 변경 없이 명령 계약을 확인한다. - -```bash -.claude/skills/deploy-batch/scripts/build-image.sh "$VERSION" --dry-run -.claude/skills/deploy-batch/scripts/push-image.sh "$VERSION" --dry-run --sign -.claude/skills/deploy-batch/scripts/update-kustomize.sh "$VERSION" both --dry-run -.claude/skills/deploy-batch/scripts/test-version-policy.sh -``` - -## 주의사항 - -- version은 prerelease/build metadata가 없는 exact `X.Y.Z`만 허용한다. -- 선택 버전은 development/production 배포 버전의 최댓값보다 커야 한다. -- 이미지 태그는 `batch_X.Y.Z`이며 기존 registry 태그를 덮어쓰지 않는다. -- kustomize update는 선택한 environment overlay만 변경한다. -- cosign 서명 시 cosign key와 password가 필요하다. diff --git a/.claude/skills/deploy-batch/scripts/build-image.sh b/.claude/skills/deploy-batch/scripts/build-image.sh deleted file mode 100755 index d6e553aca..000000000 --- a/.claude/skills/deploy-batch/scripts/build-image.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# build-image.sh -# Gradle 빌드 + Docker 이미지 빌드 - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' - -# 파라미터 파싱 -VERSION="" -DRY_RUN=false -SKIP_GRADLE=false -SEMVER_PATTERN='^(0|[1-9][0-9]*)\.((0|[1-9][0-9]*))\.((0|[1-9][0-9]*))$' - -while [[ $# -gt 0 ]]; do - case $1 in - --dry-run) - DRY_RUN=true - shift - ;; - --skip-gradle) - SKIP_GRADLE=true - shift - ;; - -*) - echo "Usage: $0 [--dry-run] [--skip-gradle]" >&2 - exit 1 - ;; - *) - [[ -z "$VERSION" ]] || { - echo "Usage: $0 [--dry-run] [--skip-gradle]" >&2 - exit 1 - } - VERSION="$1" - shift - ;; - esac -done - -[[ -n "$VERSION" ]] || { - echo -e "${RED}[ERROR]${NC} exact X.Y.Z 버전이 필요합니다" >&2 - exit 1 -} -[[ "$VERSION" =~ $SEMVER_PATTERN ]] || { - echo -e "${RED}[ERROR]${NC} 버전은 exact X.Y.Z 형식이어야 합니다: $VERSION" >&2 - exit 1 -} - -REGISTRY="docker-registry.bottle-note.com" -IMAGE_NAME="bottlenote-batch" -FULL_TAG="${REGISTRY}/${IMAGE_NAME}:batch_${VERSION}" - -echo "=== 빌드 시작 ===" -echo "버전: $VERSION" -echo "이미지: $FULL_TAG" -echo "" - -# 1. Gradle 빌드 -if [[ "$SKIP_GRADLE" == "false" ]]; then - echo -e "${GREEN}[1/2]${NC} Gradle 빌드..." - if [[ "$DRY_RUN" == "true" ]]; then - echo -e "${YELLOW}[DRY-RUN]${NC} ./gradlew :bottlenote-batch:build -x test --build-cache --parallel" - else - cd "$PROJECT_ROOT" - ./gradlew :bottlenote-batch:build -x test --build-cache --parallel - echo -e "${GREEN}[OK]${NC} Gradle 빌드 완료" - fi -else - echo -e "${YELLOW}[SKIP]${NC} Gradle 빌드 건너뜀" -fi - -# 2. Docker 빌드 -echo -e "${GREEN}[2/2]${NC} Docker 이미지 빌드..." -if [[ "$DRY_RUN" == "true" ]]; then - echo -e "${YELLOW}[DRY-RUN]${NC} docker build --platform linux/arm64 -f Dockerfile-batch -t $FULL_TAG ." -else - cd "$PROJECT_ROOT" - docker build --platform linux/arm64 -f Dockerfile-batch -t "$FULL_TAG" . - echo -e "${GREEN}[OK]${NC} Docker 빌드 완료: $FULL_TAG" -fi - -echo "" -echo "---" -echo "IMAGE=$FULL_TAG" diff --git a/.claude/skills/deploy-batch/scripts/bump-version.sh b/.claude/skills/deploy-batch/scripts/bump-version.sh deleted file mode 100755 index 25e64c43a..000000000 --- a/.claude/skills/deploy-batch/scripts/bump-version.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# 명시적 SemVer를 비파괴적으로 증가시킨다. - -SEMVER_PATTERN='^(0|[1-9][0-9]*)\.((0|[1-9][0-9]*))\.((0|[1-9][0-9]*))$' -BASE_VERSION="" -BUMP_TYPE="patch" -VALUE_ONLY=false - -while [[ $# -gt 0 ]]; do - case $1 in - --patch) - BUMP_TYPE="patch" - shift - ;; - --minor) - BUMP_TYPE="minor" - shift - ;; - --major) - BUMP_TYPE="major" - shift - ;; - --value-only) - VALUE_ONLY=true - shift - ;; - -*) - echo "Usage: $0 [--patch|--minor|--major] [--value-only]" >&2 - exit 1 - ;; - *) - [[ -z "$BASE_VERSION" ]] || { - echo "Usage: $0 [--patch|--minor|--major] [--value-only]" >&2 - exit 1 - } - BASE_VERSION=$1 - shift - ;; - esac -done - -[[ -n "$BASE_VERSION" ]] || { - echo "[ERROR] 기준 버전이 필요합니다" >&2 - exit 1 -} -[[ "$BASE_VERSION" =~ $SEMVER_PATTERN ]] || { - echo "[ERROR] 기준 버전은 exact X.Y.Z 형식이어야 합니다: $BASE_VERSION" >&2 - exit 1 -} - -IFS=. read -r major minor patch <<< "$BASE_VERSION" -case $BUMP_TYPE in - major) - new_version="$((major + 1)).0.0" - ;; - minor) - new_version="${major}.$((minor + 1)).0" - ;; - patch) - new_version="${major}.${minor}.$((patch + 1))" - ;; -esac - -if [[ "$VALUE_ONLY" == "true" ]]; then - echo "$new_version" -else - echo "BASE_VERSION=$BASE_VERSION" - echo "BUMP_TYPE=$BUMP_TYPE" - echo "VERSION=$new_version" -fi diff --git a/.claude/skills/deploy-batch/scripts/check-prerequisites.sh b/.claude/skills/deploy-batch/scripts/check-prerequisites.sh deleted file mode 100755 index 83a71499c..000000000 --- a/.claude/skills/deploy-batch/scripts/check-prerequisites.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# check-prerequisites.sh -# 배포에 필요한 도구와 키 존재 확인 -# 필수 조건 미충족 시 즉시 중단 - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' - -panic() { - echo -e "${RED}[FATAL]${NC} $1" >&2 - echo -e "${YELLOW}[해결방법]${NC} $2" >&2 - exit 1 -} - -ok() { - echo -e "${GREEN}[OK]${NC} $1" -} - -echo "=== 사전 조건 확인 ===" - -# 1. Git 설치 확인 -if command -v git &> /dev/null; then - GIT_VERSION=$(git --version) - ok "git: $GIT_VERSION" -else - panic "git 미설치" \ - "brew install git" -fi - -# 2. environment repository 확인 -ENV_REPO_DIR="$PROJECT_ROOT/git.environment-variables" -if git -C "$ENV_REPO_DIR" rev-parse --is-inside-work-tree &> /dev/null; then - ok "environment repository 초기화됨" -else - panic "environment repository가 초기화되지 않음: $ENV_REPO_DIR" \ - "git submodule update --init --recursive" -fi - -# 3. age 키 확인 -AGE_KEY_PATH="${HOME}/.config/sops/age/keys.txt" -if [[ -f "$AGE_KEY_PATH" ]]; then - ok "age 키: $AGE_KEY_PATH" -else - panic "age 키 없음: $AGE_KEY_PATH" \ - "age-keygen -o ~/.config/sops/age/keys.txt" -fi - -# 4. sops 설치 확인 -if command -v sops &> /dev/null; then - SOPS_VERSION=$(sops --version 2>&1 | head -1) - ok "sops: $SOPS_VERSION" -else - panic "sops 미설치" \ - "brew install sops" -fi - -# 5. docker 설치 확인 -if command -v docker &> /dev/null; then - DOCKER_VERSION=$(docker --version) - ok "docker: $DOCKER_VERSION" -else - panic "docker 미설치" \ - "brew install --cask docker" -fi - -# 6. kustomize 설치 확인 -if command -v kustomize &> /dev/null; then - KUSTOMIZE_VERSION=$(kustomize version 2>&1 | head -1) - ok "kustomize: $KUSTOMIZE_VERSION" -else - panic "kustomize 미설치" \ - "brew install kustomize" -fi - -# 7. cosign 설치 확인 (선택사항 - 경고만) -if command -v cosign &> /dev/null; then - COSIGN_VERSION=$(cosign version 2>&1 | grep -i version | head -1 || echo "installed") - ok "cosign: $COSIGN_VERSION" -else - echo -e "${YELLOW}[WARN]${NC} cosign 미설치 (서명 기능 제한) - brew install cosign" -fi - -# 8. registry.sops.env 파일 확인 -SOPS_ENV_FILE="$PROJECT_ROOT/git.environment-variables/storage/docker-registry/registry.sops.env" -if [[ -f "$SOPS_ENV_FILE" ]]; then - ok "registry.sops.env 존재" -else - panic "registry.sops.env 없음" \ - "git submodule update --init --recursive" -fi - -# 9. Dockerfile-batch 확인 -DOCKERFILE="$PROJECT_ROOT/Dockerfile-batch" -if [[ -f "$DOCKERFILE" ]]; then - ok "Dockerfile-batch 존재" -else - panic "Dockerfile-batch 없음: $DOCKERFILE" \ - "프로젝트 루트에 Dockerfile-batch 파일 필요" -fi - -echo "" -echo -e "${GREEN}=== 모든 사전 조건 충족 ===${NC}" -exit 0 diff --git a/.claude/skills/deploy-batch/scripts/check-version.sh b/.claude/skills/deploy-batch/scripts/check-version.sh deleted file mode 100755 index 729ea2528..000000000 --- a/.claude/skills/deploy-batch/scripts/check-version.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# 최신 배포 태그를 기준으로 다음 버전을 추천하고 exact 후보를 검증한다. - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" -ENV_REPO_DIR="${ENV_REPO_DIR:-$PROJECT_ROOT/git.environment-variables}" -SEMVER_PATTERN='^(0|[1-9][0-9]*)\.((0|[1-9][0-9]*))\.((0|[1-9][0-9]*))$' -CANDIDATE_VERSION="${1:-}" - -fail() { - echo "[ERROR] $1" >&2 - exit 1 -} - -is_semver() { - [[ "$1" =~ $SEMVER_PATTERN ]] -} - -semver_gt() { - local left=$1 - local right=$2 - local left_major left_minor left_patch - local right_major right_minor right_patch - - IFS=. read -r left_major left_minor left_patch <<< "$left" - IFS=. read -r right_major right_minor right_patch <<< "$right" - - ((left_major > right_major)) && return 0 - ((left_major < right_major)) && return 1 - ((left_minor > right_minor)) && return 0 - ((left_minor < right_minor)) && return 1 - ((left_patch > right_patch)) -} - -get_deployed_version() { - local environment=$1 - local overlay="deploy/overlays/${environment}/kustomization.yaml" - local tag - - tag=$( - git -C "$ENV_REPO_DIR" show "origin/main:${overlay}" | - awk ' - /^[[:space:]]*-[[:space:]]*name:[[:space:]]*bottlenote-batch[[:space:]]*$/ { - found = 1 - next - } - found && /^[[:space:]]*newTag:[[:space:]]*/ { - sub(/^[[:space:]]*newTag:[[:space:]]*/, "") - print - exit - } - found && /^[[:space:]]*-[[:space:]]*name:/ { - exit - } - ' - ) || fail "${environment} overlay를 origin/main에서 읽지 못했습니다" - - [[ "$tag" == batch_* ]] || - fail "${environment} batch 태그가 없거나 batch_X.Y.Z 형식이 아닙니다" - - local version=${tag#batch_} - is_semver "$version" || - fail "${environment} batch 태그가 exact SemVer가 아닙니다: $tag" - - printf '%s\n' "$version" -} - -[[ -d "$ENV_REPO_DIR" ]] || - fail "environment repository가 없습니다: $ENV_REPO_DIR" -git -C "$ENV_REPO_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1 || - fail "environment repository가 초기화되지 않았습니다: $ENV_REPO_DIR" -git -C "$ENV_REPO_DIR" fetch --quiet origin main || - fail "environment repository origin/main fetch에 실패했습니다" - -PRODUCTION_VERSION=$(get_deployed_version production) -DEVELOPMENT_VERSION=$(get_deployed_version development) - -BASE_VERSION=$PRODUCTION_VERSION -if semver_gt "$DEVELOPMENT_VERSION" "$PRODUCTION_VERSION"; then - BASE_VERSION=$DEVELOPMENT_VERSION -fi - -RECOMMENDED_VERSION=$( - "$SCRIPT_DIR/bump-version.sh" "$BASE_VERSION" --patch --value-only -) - -if [[ -n "$CANDIDATE_VERSION" ]]; then - is_semver "$CANDIDATE_VERSION" || - fail "입력 버전은 exact X.Y.Z 형식이어야 합니다: $CANDIDATE_VERSION" - semver_gt "$CANDIDATE_VERSION" "$BASE_VERSION" || - fail "입력 버전은 현재 최댓값 $BASE_VERSION 보다 커야 합니다" -fi - -echo "PRODUCTION_VERSION=$PRODUCTION_VERSION" -echo "DEVELOPMENT_VERSION=$DEVELOPMENT_VERSION" -echo "BASE_VERSION=$BASE_VERSION" -echo "RECOMMENDED_VERSION=$RECOMMENDED_VERSION" -if [[ -n "$CANDIDATE_VERSION" ]]; then - echo "CANDIDATE_VERSION=$CANDIDATE_VERSION" -fi -echo "STATUS=OK" diff --git a/.claude/skills/deploy-batch/scripts/decrypt-registry.sh b/.claude/skills/deploy-batch/scripts/decrypt-registry.sh deleted file mode 100755 index 32ecf3157..000000000 --- a/.claude/skills/deploy-batch/scripts/decrypt-registry.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# decrypt-registry.sh -# sops로 레지스트리 인증 정보 복호화 - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" - -SOPS_ENV_FILE="$PROJECT_ROOT/git.environment-variables/storage/docker-registry/registry.sops.env" - -# 옵션 파싱 -EXPORT_MODE=false -while [[ $# -gt 0 ]]; do - case $1 in - --export) - EXPORT_MODE=true - shift - ;; - *) - echo "Usage: $0 [--export]" - echo " --export: export 문 형태로 출력 (source로 사용 가능)" - exit 1 - ;; - esac -done - -# age 키 존재 확인 -AGE_KEY_PATH="${HOME}/.config/sops/age/keys.txt" -if [[ ! -f "$AGE_KEY_PATH" ]]; then - echo "ERROR: age 키 없음: $AGE_KEY_PATH" >&2 - exit 1 -fi - -# sops.env 파일 존재 확인 -if [[ ! -f "$SOPS_ENV_FILE" ]]; then - echo "ERROR: registry.sops.env 없음: $SOPS_ENV_FILE" >&2 - exit 1 -fi - -# sops로 복호화 -DECRYPTED=$(sops -d "$SOPS_ENV_FILE" 2>/dev/null) - -# 파싱 -REGISTRY_ADDRESS=$(echo "$DECRYPTED" | grep "^REGISTRY_ADDRESS=" | cut -d'=' -f2-) -REGISTRY_USERNAME=$(echo "$DECRYPTED" | grep "^REGISTRY_USERNAME=" | cut -d'=' -f2-) -REGISTRY_PASSWORD=$(echo "$DECRYPTED" | grep "^REGISTRY_PASSWORD=" | cut -d'=' -f2-) -COSIGN_PASSWORD=$(echo "$DECRYPTED" | grep "^COSIGN_PASSWORD=" | cut -d'=' -f2-) - -if [[ -z "$REGISTRY_ADDRESS" ]] || [[ -z "$REGISTRY_USERNAME" ]] || [[ -z "$REGISTRY_PASSWORD" ]]; then - echo "ERROR: 복호화 실패 또는 필수 필드 누락" >&2 - exit 1 -fi - -if [[ "$EXPORT_MODE" == "true" ]]; then - # source로 사용 가능한 형태로 출력 - echo "export REGISTRY_ADDRESS=\"$REGISTRY_ADDRESS\"" - echo "export REGISTRY_USERNAME=\"$REGISTRY_USERNAME\"" - echo "export REGISTRY_PASSWORD=\"$REGISTRY_PASSWORD\"" - [[ -n "$COSIGN_PASSWORD" ]] && echo "export COSIGN_PASSWORD=\"$COSIGN_PASSWORD\"" -else - # 일반 출력 - echo "REGISTRY_ADDRESS=$REGISTRY_ADDRESS" - echo "REGISTRY_USERNAME=$REGISTRY_USERNAME" - echo "REGISTRY_PASSWORD=***" - echo "COSIGN_PASSWORD=${COSIGN_PASSWORD:+***}" - echo "" - echo "복호화 성공. 전체 값을 보려면 --export 옵션 사용" -fi diff --git a/.claude/skills/deploy-batch/scripts/push-image.sh b/.claude/skills/deploy-batch/scripts/push-image.sh deleted file mode 100755 index af9b2fb3e..000000000 --- a/.claude/skills/deploy-batch/scripts/push-image.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# push-image.sh -# Docker 푸시 및 선택적 cosign 서명 - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' - -# 파라미터 파싱 -VERSION="" -DRY_RUN=false -SIGN=false -SEMVER_PATTERN='^(0|[1-9][0-9]*)\.((0|[1-9][0-9]*))\.((0|[1-9][0-9]*))$' - -while [[ $# -gt 0 ]]; do - case $1 in - --dry-run) - DRY_RUN=true - shift - ;; - --sign) - SIGN=true - shift - ;; - -*) - echo "Usage: $0 [--dry-run] [--sign]" >&2 - exit 1 - ;; - *) - [[ -z "$VERSION" ]] || { - echo "Usage: $0 [--dry-run] [--sign]" >&2 - exit 1 - } - VERSION="$1" - shift - ;; - esac -done - -[[ -n "$VERSION" ]] || { - echo -e "${RED}[ERROR]${NC} exact X.Y.Z 버전이 필요합니다" >&2 - exit 1 -} -[[ "$VERSION" =~ $SEMVER_PATTERN ]] || { - echo -e "${RED}[ERROR]${NC} 버전은 exact X.Y.Z 형식이어야 합니다: $VERSION" >&2 - exit 1 -} - -REGISTRY="docker-registry.bottle-note.com" -IMAGE_NAME="bottlenote-batch" -FULL_TAG="${REGISTRY}/${IMAGE_NAME}:batch_${VERSION}" - -echo "=== Docker 푸시 ===" -echo "이미지: $FULL_TAG" -echo "서명: $SIGN" -echo "" - -# 레지스트리 인증 정보 복호화 -echo -e "${GREEN}[1/3]${NC} 레지스트리 인증..." -if [[ "$DRY_RUN" == "true" ]]; then - echo -e "${YELLOW}[DRY-RUN]${NC} decrypt-registry.sh --export" - REGISTRY_ADDRESS="docker-registry.bottle-note.com" - REGISTRY_USERNAME="test" - REGISTRY_PASSWORD="test" -else - DECRYPT_OUTPUT=$("$SCRIPT_DIR/decrypt-registry.sh" --export) - eval "$DECRYPT_OUTPUT" - - # Docker 로그인 - echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_ADDRESS" -u "$REGISTRY_USERNAME" --password-stdin - echo -e "${GREEN}[OK]${NC} Docker 로그인 완료" -fi - -# Docker 푸시 -echo -e "${GREEN}[2/3]${NC} Docker 이미지 푸시..." -if [[ "$DRY_RUN" == "true" ]]; then - echo -e "${YELLOW}[DRY-RUN]${NC} docker push $FULL_TAG" -else - docker push "$FULL_TAG" - echo -e "${GREEN}[OK]${NC} 푸시 완료: $FULL_TAG" -fi - -# cosign 서명 (선택) -if [[ "$SIGN" == "true" ]]; then - echo -e "${GREEN}[3/3]${NC} cosign 서명..." - - COSIGN_KEY="$PROJECT_ROOT/git.environment-variables/storage/docker-registry/cosign.key" - - if [[ ! -f "$COSIGN_KEY" ]]; then - echo -e "${RED}[ERROR]${NC} cosign 키 없음: $COSIGN_KEY" >&2 - exit 1 - fi - - if ! command -v cosign &> /dev/null; then - echo -e "${RED}[ERROR]${NC} cosign 미설치" >&2 - exit 1 - fi - - if [[ "$DRY_RUN" == "true" ]]; then - echo -e "${YELLOW}[DRY-RUN]${NC} COSIGN_PASSWORD=*** cosign sign --key $COSIGN_KEY $FULL_TAG" - else - # COSIGN_PASSWORD는 decrypt-registry.sh에서 export됨 - if [[ -z "${COSIGN_PASSWORD:-}" ]]; then - echo -e "${RED}[ERROR]${NC} COSIGN_PASSWORD 없음 (registry.sops.env에 추가 필요)" >&2 - exit 1 - fi - export COSIGN_PASSWORD - cosign sign --key "$COSIGN_KEY" "$FULL_TAG" - echo -e "${GREEN}[OK]${NC} cosign 서명 완료" - fi -else - echo -e "${YELLOW}[SKIP]${NC} cosign 서명 건너뜀 (--sign 옵션 없음)" -fi - -echo "" -echo "---" -echo "PUSHED=$FULL_TAG" -echo "SIGNED=$SIGN" diff --git a/.claude/skills/deploy-batch/scripts/test-version-policy.sh b/.claude/skills/deploy-batch/scripts/test-version-policy.sh deleted file mode 100755 index a80c63b0d..000000000 --- a/.claude/skills/deploy-batch/scripts/test-version-policy.sh +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# 로컬 Git fixture로 배포 버전 정책을 회귀 검증한다. - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" -AGENTS_SKILL="$PROJECT_ROOT/.agents/skills/deploy-batch" -CLAUDE_SKILL="$PROJECT_ROOT/.claude/skills/deploy-batch" -TEMP_DIR=$(mktemp -d) -trap 'rm -rf "$TEMP_DIR"' EXIT - -fail() { - echo "[FAIL] $1" >&2 - exit 1 -} - -assert_output() { - local output=$1 - local expected=$2 - grep -Fqx "$expected" <<< "$output" || - fail "출력 누락: $expected" -} - -expect_failure() { - if "$@" >"$TEMP_DIR/expected-failure.out" 2>&1; then - fail "실패해야 하는 명령이 성공했습니다: $*" - fi -} - -write_overlay() { - local repository=$1 - local environment=$2 - local tag=$3 - local overlay_dir="$repository/deploy/overlays/$environment" - - mkdir -p "$overlay_dir" - printf '%s\n' \ - 'images:' \ - '- name: bottlenote-batch' \ - ' newName: docker-registry.bottle-note.com/bottlenote-batch' \ - " newTag: $tag" \ - > "$overlay_dir/kustomization.yaml" -} - -commit_fixture() { - local message=$1 - - git -C "$TEMP_DIR/seed" add deploy - git -C "$TEMP_DIR/seed" commit -q -m "$message" - git -C "$TEMP_DIR/seed" push -q origin main -} - -git init -q --bare "$TEMP_DIR/remote.git" -git init -q -b main "$TEMP_DIR/seed" -git -C "$TEMP_DIR/seed" config user.name "Batch Version Test" -git -C "$TEMP_DIR/seed" config user.email "batch-version-test@example.com" -git -C "$TEMP_DIR/seed" remote add origin "$TEMP_DIR/remote.git" - -write_overlay "$TEMP_DIR/seed" production batch_1.0.6 -write_overlay "$TEMP_DIR/seed" development batch_1.0.6 -commit_fixture "initial tags" -git clone -q "$TEMP_DIR/remote.git" "$TEMP_DIR/environment" - -output=$(ENV_REPO_DIR="$TEMP_DIR/environment" "$SCRIPT_DIR/check-version.sh") -assert_output "$output" "PRODUCTION_VERSION=1.0.6" -assert_output "$output" "DEVELOPMENT_VERSION=1.0.6" -assert_output "$output" "BASE_VERSION=1.0.6" -assert_output "$output" "RECOMMENDED_VERSION=1.0.7" - -write_overlay "$TEMP_DIR/seed" production batch_1.9.9 -write_overlay "$TEMP_DIR/seed" development batch_2.0.0 -commit_fixture "different tags" - -output=$(ENV_REPO_DIR="$TEMP_DIR/environment" "$SCRIPT_DIR/check-version.sh" 2.1.0) -assert_output "$output" "BASE_VERSION=2.0.0" -assert_output "$output" "RECOMMENDED_VERSION=2.0.1" -assert_output "$output" "CANDIDATE_VERSION=2.1.0" -expect_failure env ENV_REPO_DIR="$TEMP_DIR/environment" "$SCRIPT_DIR/check-version.sh" 2.0.0 -expect_failure env ENV_REPO_DIR="$TEMP_DIR/environment" "$SCRIPT_DIR/check-version.sh" 2.0.1-rc.1 - -write_overlay "$TEMP_DIR/seed" production batch_invalid -commit_fixture "invalid tag" -expect_failure env ENV_REPO_DIR="$TEMP_DIR/environment" "$SCRIPT_DIR/check-version.sh" - -write_overlay "$TEMP_DIR/seed" production batch_2.0.0 -write_overlay "$TEMP_DIR/seed" development other_2.0.0 -commit_fixture "missing batch tag" -expect_failure env ENV_REPO_DIR="$TEMP_DIR/environment" "$SCRIPT_DIR/check-version.sh" - -git -C "$TEMP_DIR/environment" remote set-url origin "$TEMP_DIR/missing.git" -expect_failure env ENV_REPO_DIR="$TEMP_DIR/environment" "$SCRIPT_DIR/check-version.sh" - -[[ "$("$SCRIPT_DIR/bump-version.sh" 1.0.6 --patch --value-only)" == "1.0.7" ]] || - fail "patch 증가 실패" -[[ "$("$SCRIPT_DIR/bump-version.sh" 1.0.6 --minor --value-only)" == "1.1.0" ]] || - fail "minor 증가 실패" -[[ "$("$SCRIPT_DIR/bump-version.sh" 1.0.6 --major --value-only)" == "2.0.0" ]] || - fail "major 증가 실패" - -for script in build-image.sh push-image.sh update-kustomize.sh; do - expect_failure "$SCRIPT_DIR/$script" - expect_failure "$SCRIPT_DIR/$script" 1.0.7-rc.1 --dry-run -done -"$SCRIPT_DIR/build-image.sh" 1.0.7 --dry-run --skip-gradle >/dev/null -"$SCRIPT_DIR/push-image.sh" 1.0.7 --dry-run >/dev/null -"$SCRIPT_DIR/update-kustomize.sh" 1.0.7 both --dry-run >/dev/null - -for script in "$AGENTS_SKILL"/scripts/*.sh "$CLAUDE_SKILL"/scripts/*.sh; do - bash -n "$script" -done - -obsolete_pattern='VERSION_''FILE|bottlenote-batch/VER''SION' -if grep -R -E "$obsolete_pattern" "$AGENTS_SKILL" "$CLAUDE_SKILL"; then - fail "VERSION 파일 참조가 남아 있습니다" -fi -version_file="$PROJECT_ROOT/bottlenote-batch/VER""SION" -[[ ! -e "$version_file" ]] || - fail "bottlenote-batch/VER""SION 파일이 남아 있습니다" - -workflow="$PROJECT_ROOT/.github/workflows/deploy_batch.yml" -grep -A3 '^ version:' "$workflow" | grep -q 'required: true' || - fail "workflow required version input이 없습니다" -grep -q 'INPUT_VERSION:.*inputs.version' "$workflow" || - fail "workflow version input 연결이 없습니다" -[[ "$(grep -c 'docker manifest inspect' "$workflow")" -eq 2 ]] || - fail "immutable image 검사는 두 단계에 있어야 합니다" - -find "$AGENTS_SKILL" -type f | sed "s#^$AGENTS_SKILL/##" | sort > "$TEMP_DIR/agents-files" -find "$CLAUDE_SKILL" -type f | sed "s#^$CLAUDE_SKILL/##" | sort > "$TEMP_DIR/claude-files" -cmp -s "$TEMP_DIR/agents-files" "$TEMP_DIR/claude-files" || - fail "deploy-batch 복제본 파일 목록이 다릅니다" - -while IFS= read -r relative_path; do - agents_hash=$(shasum -a 256 "$AGENTS_SKILL/$relative_path" | awk '{print $1}') - claude_hash=$(shasum -a 256 "$CLAUDE_SKILL/$relative_path" | awk '{print $1}') - [[ "$agents_hash" == "$claude_hash" ]] || - fail "deploy-batch 복제본 SHA-256 불일치: $relative_path" -done < "$TEMP_DIR/agents-files" - -echo "PASS: batch version policy" diff --git a/.claude/skills/deploy-batch/scripts/update-kustomize.sh b/.claude/skills/deploy-batch/scripts/update-kustomize.sh deleted file mode 100755 index 16432e88a..000000000 --- a/.claude/skills/deploy-batch/scripts/update-kustomize.sh +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# update-kustomize.sh -# kustomization.yaml 업데이트 및 서브모듈 푸시 - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' - -# 파라미터 파싱 -VERSION="" -ENV="both" -DRY_RUN=false -SEMVER_PATTERN='^(0|[1-9][0-9]*)\.((0|[1-9][0-9]*))\.((0|[1-9][0-9]*))$' - -while [[ $# -gt 0 ]]; do - case $1 in - --dry-run) - DRY_RUN=true - shift - ;; - production|development|both) - ENV="$1" - shift - ;; - -*) - echo "Usage: $0 [production|development|both] [--dry-run]" >&2 - exit 1 - ;; - *) - [[ -z "$VERSION" ]] || { - echo "Usage: $0 [production|development|both] [--dry-run]" >&2 - exit 1 - } - VERSION="$1" - shift - ;; - esac -done - -[[ -n "$VERSION" ]] || { - echo -e "${RED}[ERROR]${NC} exact X.Y.Z 버전이 필요합니다" >&2 - exit 1 -} -[[ "$VERSION" =~ $SEMVER_PATTERN ]] || { - echo -e "${RED}[ERROR]${NC} 버전은 exact X.Y.Z 형식이어야 합니다: $VERSION" >&2 - exit 1 -} - -# 환경 파라미터 검증 -if [[ "$ENV" != "production" ]] && [[ "$ENV" != "development" ]] && [[ "$ENV" != "both" ]]; then - echo -e "${RED}[ERROR]${NC} 잘못된 환경: $ENV (production|development|both)" >&2 - exit 1 -fi - -REGISTRY="docker-registry.bottle-note.com" -IMAGE_NAME="bottlenote-batch" -FULL_TAG="batch_${VERSION}" -SUBMODULE_DIR="$PROJECT_ROOT/git.environment-variables" - -echo "=== Kustomize 업데이트 ===" -echo "버전: $VERSION" -echo "태그: $FULL_TAG" -echo "환경: $ENV" -echo "" - -# 서브모듈 동기화 (최우선) -echo -e "${GREEN}[SYNC]${NC} 서브모듈 원격 동기화..." -if [[ "$DRY_RUN" == "true" ]]; then - echo -e "${YELLOW}[DRY-RUN]${NC} cd $SUBMODULE_DIR && git fetch origin && git reset --hard origin/main" -else - cd "$SUBMODULE_DIR" - - # 현재 브랜치 확인 - CURRENT_BRANCH=$(git branch --show-current) - if [[ "$CURRENT_BRANCH" != "main" ]]; then - echo -e "${YELLOW}[WARN]${NC} 현재 브랜치: $CURRENT_BRANCH (main이 아님)" - git checkout main - fi - - # 원격에서 최신 상태 가져오기 - git fetch origin - - # 로컬 변경사항 확인 및 처리 - if ! git diff --quiet || ! git diff --cached --quiet; then - echo -e "${YELLOW}[WARN]${NC} 로컬 변경사항 있음 - 리셋합니다" - git reset --hard origin/main - else - # 변경사항 없으면 fast-forward - git reset --hard origin/main - fi - - echo -e "${GREEN}[OK]${NC} 서브모듈 동기화 완료 (origin/main)" -fi -echo "" - -update_kustomize() { - local env=$1 - local overlay_dir="$SUBMODULE_DIR/deploy/overlays/$env" - - if [[ ! -d "$overlay_dir" ]]; then - echo -e "${RED}[ERROR]${NC} 디렉토리 없음: $overlay_dir" >&2 - return 1 - fi - - echo -e "${GREEN}[$env]${NC} kustomize 업데이트..." - - if [[ "$DRY_RUN" == "true" ]]; then - echo -e "${YELLOW}[DRY-RUN]${NC} cd $overlay_dir && kustomize edit set image bottlenote-batch=${REGISTRY}/${IMAGE_NAME}:${FULL_TAG}" - else - cd "$overlay_dir" - kustomize edit set image "bottlenote-batch=${REGISTRY}/${IMAGE_NAME}:${FULL_TAG}" - echo -e "${GREEN}[OK]${NC} $env 업데이트 완료" - fi -} - -# 환경별 업데이트 -if [[ "$ENV" == "both" ]] || [[ "$ENV" == "production" ]]; then - update_kustomize "production" -fi - -if [[ "$ENV" == "both" ]] || [[ "$ENV" == "development" ]]; then - update_kustomize "development" -fi - -# 서브모듈 커밋 및 푸시 -echo "" -echo -e "${GREEN}[COMMIT]${NC} 서브모듈 커밋 및 푸시..." - -if [[ "$DRY_RUN" == "true" ]]; then - echo -e "${YELLOW}[DRY-RUN]${NC} cd $SUBMODULE_DIR" - echo -e "${YELLOW}[DRY-RUN]${NC} git add -A" - echo -e "${YELLOW}[DRY-RUN]${NC} git commit -m 'chore(batch): update image tag to ${FULL_TAG}'" - echo -e "${YELLOW}[DRY-RUN]${NC} git push origin main" -else - cd "$SUBMODULE_DIR" - - # 변경사항 확인 - if git diff --quiet && git diff --cached --quiet; then - echo -e "${YELLOW}[SKIP]${NC} 변경사항 없음" - else - git add -A - git commit -m "chore(batch): update image tag to ${FULL_TAG}" - git push origin main - echo -e "${GREEN}[OK]${NC} 서브모듈 푸시 완료" - fi -fi - -echo "" -echo "---" -echo "VERSION=$VERSION" -echo "TAG=$FULL_TAG" -echo "ENVIRONMENT=$ENV" diff --git a/.claude/skills/implement/references/languages/java-spring.md b/.claude/skills/implement/references/languages/java-spring.md index 07036621c..9204c1394 100644 --- a/.claude/skills/implement/references/languages/java-spring.md +++ b/.claude/skills/implement/references/languages/java-spring.md @@ -222,7 +222,7 @@ Pattern: async + `REQUIRES_NEW` separates side-effect transaction from main. Fai | Annotation | Role | Location | Includes | |------------|------|----------|----------| -| `@FacadeService` | Aggregate Root external door | `{domain}/facade/` | `@Service` | +| `@FacadeService` | Aggregate Root external door | impl `Default{Domain}Facade` in `{domain}/service/`, interface in `{domain}/facade/` | `@Service` | | `@DomainRepository` | Pure domain interface | `{domain}/domain/` | (none, marker) | | `@JpaRepositoryImpl` | JPA implementation | `{domain}/repository/` | `@Repository` | | `@DomainEventListener` | Domain event listener | `{domain}/event/` | `@Component` | diff --git a/.codex/hooks.json b/.codex/hooks.json deleted file mode 100644 index ca739d585..000000000 --- a/.codex/hooks.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "hooks": { - "PostToolUse": [ - { - "matcher": "Edit|Write", - "hooks": [ - { - "type": "command", - "command": "FP=$(cat | jq -r '.tool_input.file_path // empty'); [[ \"$FP\" == *.java || \"$FP\" == *.kt ]] && cd $CLAUDE_PROJECT_DIR && ./gradlew spotlessApply -q 2>/dev/null || true", - "timeout": 30 - } - ] - } - ], - "SessionStart": [ - { - "hooks": [ - { - "type": "command", - "command": "'C:\\Users\\rlagu\\workspace\\bottlenote\\bottle-note-api-server\\.codex\\hooks\\session-start.sh'" - } - ] - } - ] - } -} diff --git a/.codex/hooks/session-start.sh b/.codex/hooks/session-start.sh deleted file mode 100644 index df943d91e..000000000 --- a/.codex/hooks/session-start.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Claude Code 웹 환경에서만 실행 -if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then - exit 0 -fi - -echo "🚀 Starting Docker setup for TestContainers..." - -# Docker가 이미 설치되어 있는지 확인 -if command -v docker &> /dev/null; then - echo "✅ Docker already installed: $(docker --version)" - - # Docker 데몬이 실행 중인지 확인 - if docker info &> /dev/null; then - echo "✅ Docker daemon is already running" - exit 0 - else - echo "🔄 Docker daemon not running, starting..." - fi -else - echo "📦 Installing Docker..." - - # 문제가 되는 PPA 저장소 제거 - rm -f /etc/apt/sources.list.d/deadsnakes-ubuntu-ppa-noble.sources - rm -f /etc/apt/sources.list.d/ondrej-ubuntu-php-noble.sources - - # /tmp 권한 수정 - chmod 1777 /tmp 2>/dev/null || true - - # apt 업데이트 - apt-get update -y -qq - - # Docker GPG 키 및 저장소 추가 - install -m 0755 -d /etc/apt/keyrings - curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc - chmod a+r /etc/apt/keyrings/docker.asc - - echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable" > /etc/apt/sources.list.d/docker.list - - # apt 업데이트 및 Docker 설치 - apt-get update -y -qq - DEBIAN_FRONTEND=noninteractive apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin > /dev/null - - echo "✅ Docker installed successfully: $(docker --version)" -fi - -# Docker 데몬 시작 (vfs storage driver 사용) -if ! docker info &> /dev/null; then - echo "🔄 Starting Docker daemon..." - - # 기존 docker0 인터페이스 정리 (있는 경우) - if [ -d /sys/class/net/docker0 ]; then - echo "🧹 Cleaning up existing docker0 interface..." - ip link delete docker0 2>/dev/null || ifconfig docker0 down 2>/dev/null || true - fi - - # dockerd 프로세스가 이미 실행 중인지 확인 - if pidof dockerd > /dev/null; then - echo "⏳ Docker daemon process found, waiting for it to be ready..." - sleep 3 - else - dockerd --iptables=false --ip-masq=false --storage-driver=vfs > /var/log/dockerd.log 2>&1 & - fi - - # Docker 데몬이 준비될 때까지 대기 - for i in {1..15}; do - if docker info &> /dev/null; then - echo "✅ Docker daemon started successfully" - break - fi - echo "⏳ Waiting for Docker daemon to start... ($i/15)" - sleep 2 - done - - if ! docker info &> /dev/null; then - echo "❌ Failed to start Docker daemon" - echo "📋 Docker logs:" - tail -30 /var/log/dockerd.log - exit 1 - fi -fi - -echo "🎉 Docker setup completed successfully!" -docker info 2>&1 | head -5 || true diff --git a/.github/workflows/deploy_v2_admin_api.yml.bak b/.github/workflows/deploy_v2_admin_api.yml.bak deleted file mode 100644 index 696672051..000000000 --- a/.github/workflows/deploy_v2_admin_api.yml.bak +++ /dev/null @@ -1,204 +0,0 @@ -name: deploy admin api on release publish - -on: - release: - types: - - published - workflow_dispatch: - inputs: - tag: - description: 'Admin release tag, for example admin/v1.2.3' - required: true - type: string - -permissions: - contents: read - actions: read - -concurrency: - group: deploy-admin-production-${{ github.event.release.tag_name || inputs.tag }} - cancel-in-progress: true - -env: - IMAGE_NAME: bottlenote-admin-api - -jobs: - verify-admin-release: - name: verify admin release publish - runs-on: ubuntu-latest - outputs: - release-tag: ${{ steps.release.outputs.release-tag }} - version: ${{ steps.release.outputs.version }} - image-tag: ${{ steps.release.outputs.image-tag }} - tag-sha: ${{ steps.release.outputs.tag-sha }} - steps: - - name: checkout repository - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: resolve admin release - id: release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - EVENT_NAME: ${{ github.event_name }} - EVENT_TAG: ${{ github.event.release.tag_name || '' }} - EVENT_DRAFT: ${{ github.event.release.draft || '' }} - EVENT_PRERELEASE: ${{ github.event.release.prerelease || '' }} - INPUT_TAG: ${{ inputs.tag || '' }} - run: | - set -euo pipefail - release_tag="${EVENT_TAG:-$INPUT_TAG}" - [[ "$release_tag" =~ ^admin/v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::admin release tag required, got: $release_tag"; exit 1; } - version="${release_tag#admin/v}" - image_tag="admin_${version}" - - git fetch --no-tags origin '+refs/heads/main:refs/remotes/origin/main' - git fetch --tags origin - git rev-parse -q --verify "refs/tags/${release_tag}" >/dev/null || { echo "::error::release tag not found: $release_tag"; exit 1; } - tag_sha="$(git rev-list -n 1 "$release_tag")" - git merge-base --is-ancestor "$tag_sha" origin/main || { echo "::error::tag commit is not contained in origin/main: $tag_sha"; exit 1; } - - releases_json="$(curl -fsSL \ - -H "Authorization: Bearer ${GH_TOKEN}" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases?per_page=100")" - RELEASES_JSON="$releases_json" RELEASE_TAG="$release_tag" python3 - <<'PY' - import json, os, sys - tag = os.environ['RELEASE_TAG'] - releases = json.loads(os.environ['RELEASES_JSON']) - release = next((item for item in releases if item.get('tag_name') == tag), None) - if release is None: - print(f'::error::release not found for tag: {tag}', file=sys.stderr) - sys.exit(1) - if release.get('draft'): - print('::error::release must be published', file=sys.stderr) - sys.exit(1) - if release.get('prerelease'): - print('::error::production release must not be prerelease', file=sys.stderr) - sys.exit(1) - print(f"release_id={release.get('id')}") - PY - - { - echo "release-tag=$release_tag" - echo "version=$version" - echo "image-tag=$image_tag" - echo "tag-sha=$tag_sha" - } >> "$GITHUB_OUTPUT" - echo "[admin-release-deploy] release_tag=$release_tag version=$version image_tag=$image_tag tag_sha=$tag_sha" - - build-and-push: - name: build and push admin image - needs: verify-admin-release - runs-on: ubuntu-24.04-arm - outputs: - image-tag: ${{ needs.verify-admin-release.outputs.image-tag }} - image-digest: ${{ steps.build.outputs.image-digest }} - steps: - - name: checkout release tag with submodules - uses: actions/checkout@v7 - with: - ref: ${{ needs.verify-admin-release.outputs.release-tag }} - submodules: true - token: ${{ secrets.GIT_ACCESS_TOKEN }} - - - name: setup jdk - uses: actions/setup-java@v5 - with: - java-version: '21' - distribution: 'temurin' - - - name: setup gradle cache - uses: actions/cache@v6 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: build admin module - run: | - ./gradlew :bottlenote-admin-api:build -x test -x asciidoctor --build-cache --parallel - - - name: refresh docker build action from main - run: | - git fetch --no-tags origin '+refs/heads/main:refs/remotes/origin/main' - git checkout origin/main -- .github/actions/docker-build-push/action.yml - - - name: build and push to registry - id: build - uses: ./.github/actions/docker-build-push - with: - registry-url: ${{ secrets.REGISTRY_ADDRESS }} - registry-username: ${{ secrets.REGISTRY_USERNAME }} - registry-password: ${{ secrets.REGISTRY_PASSWORD }} - image-name: ${{ env.IMAGE_NAME }} - image-tag: ${{ needs.verify-admin-release.outputs.image-tag }} - additional-tags: admin_latest - dockerfile: Dockerfile-admin - context: . - platforms: linux/arm64 - cache-scope: admin-production - image-title: "BottleNote Admin API" - image-description: "BottleNote Admin API Server" - image-vendor: "BottleNote" - image-authors: "BottleNote Team" - sign-image: 'true' - cosign-key-path: 'git.environment-variables/storage/docker-registry/cosign.key' - cosign-password: ${{ secrets.COSIGN_PASSWORD }} - - update-production: - name: update admin production image - needs: - - verify-admin-release - - build-and-push - runs-on: ubuntu-latest - steps: - - name: checkout code with submodules - uses: actions/checkout@v7 - with: - submodules: true - token: ${{ secrets.GIT_ACCESS_TOKEN }} - - - name: setup kustomize - uses: imranismail/setup-kustomize@v3 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: update production image tag - run: | - IMAGE_TAG="${{ needs.build-and-push.outputs.image-tag }}" - REGISTRY="${{ secrets.REGISTRY_ADDRESS }}" - echo "Updating admin production image tag: $IMAGE_TAG" - cd git.environment-variables - git checkout main - cd deploy/overlays/production - kustomize edit set image bottlenote-admin-api=${REGISTRY}/${{ env.IMAGE_NAME }}:${IMAGE_TAG} - echo "production kustomization.yaml updated" - - - name: commit and push with retry - run: | - cd git.environment-variables - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add deploy/overlays/production/kustomization.yaml - if git diff --staged --quiet; then - echo "No changes to commit" - exit 0 - fi - git commit -m "chore(admin): update production image tag to ${{ needs.build-and-push.outputs.image-tag }} - - Updated by GitHub Actions - Release: ${{ needs.verify-admin-release.outputs.release-tag }} - Commit: ${{ needs.verify-admin-release.outputs.tag-sha }} - Workflow: ${{ github.workflow }}" - for i in {1..5}; do - git pull --rebase origin main && git push origin main && break - echo "Push failed, retry $i/5..." - sleep $((i * 2)) - done - echo "Admin production deployment triggered" diff --git a/.github/workflows/deploy_v2_release_product_api.yml.bak b/.github/workflows/deploy_v2_release_product_api.yml.bak deleted file mode 100644 index 101201b71..000000000 --- a/.github/workflows/deploy_v2_release_product_api.yml.bak +++ /dev/null @@ -1,201 +0,0 @@ -name: deploy v2 product api production - -on: - release: - types: - - published - workflow_dispatch: - inputs: - tag: - description: 'Product release tag, for example product/v1.2.3' - required: true - type: string - -permissions: - contents: read - actions: read - -concurrency: - group: deploy-product-production-${{ github.event.release.tag_name || inputs.tag }} - cancel-in-progress: true - -env: - IMAGE_NAME: bottlenote-product-api - -jobs: - verify-product-release: - name: verify product release publish - runs-on: ubuntu-latest - outputs: - release-tag: ${{ steps.release.outputs.release-tag }} - version: ${{ steps.release.outputs.version }} - image-tag: ${{ steps.release.outputs.image-tag }} - tag-sha: ${{ steps.release.outputs.tag-sha }} - steps: - - name: checkout repository - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: resolve product release - id: release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - EVENT_TAG: ${{ github.event.release.tag_name || '' }} - INPUT_TAG: ${{ inputs.tag || '' }} - run: | - set -euo pipefail - release_tag="${EVENT_TAG:-$INPUT_TAG}" - [[ "$release_tag" =~ ^product/v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::product release tag required, got: $release_tag"; exit 1; } - version="${release_tag#product/v}" - image_tag="product_${version}" - - git fetch --no-tags origin '+refs/heads/main:refs/remotes/origin/main' - git fetch --tags origin - git rev-parse -q --verify "refs/tags/${release_tag}" >/dev/null || { echo "::error::release tag not found: $release_tag"; exit 1; } - tag_sha="$(git rev-list -n 1 "$release_tag")" - git merge-base --is-ancestor "$tag_sha" origin/main || { echo "::error::tag commit is not contained in origin/main: $tag_sha"; exit 1; } - - releases_json="$(curl -fsSL \ - -H "Authorization: Bearer ${GH_TOKEN}" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases?per_page=100")" - RELEASES_JSON="$releases_json" RELEASE_TAG="$release_tag" python3 - <<'PY' - import json, os, sys - tag = os.environ['RELEASE_TAG'] - releases = json.loads(os.environ['RELEASES_JSON']) - release = next((item for item in releases if item.get('tag_name') == tag), None) - if release is None: - print(f'::error::release not found for tag: {tag}', file=sys.stderr) - sys.exit(1) - if release.get('draft'): - print('::error::release must be published', file=sys.stderr) - sys.exit(1) - if release.get('prerelease'): - print('::error::production release must not be prerelease', file=sys.stderr) - sys.exit(1) - print(f"release_id={release.get('id')}") - PY - - { - echo "release-tag=$release_tag" - echo "version=$version" - echo "image-tag=$image_tag" - echo "tag-sha=$tag_sha" - } >> "$GITHUB_OUTPUT" - echo "[product-release-deploy] release_tag=$release_tag version=$version image_tag=$image_tag tag_sha=$tag_sha" - - build-and-push: - name: build and push product image - needs: verify-product-release - runs-on: ubuntu-24.04-arm - outputs: - image-tag: ${{ needs.verify-product-release.outputs.image-tag }} - image-digest: ${{ steps.build.outputs.image-digest }} - steps: - - name: checkout release tag with submodules - uses: actions/checkout@v7 - with: - ref: ${{ needs.verify-product-release.outputs.release-tag }} - submodules: true - token: ${{ secrets.GIT_ACCESS_TOKEN }} - - - name: setup jdk - uses: actions/setup-java@v5 - with: - java-version: '21' - distribution: 'temurin' - - - name: setup gradle cache - uses: actions/cache@v6 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: build product module - run: | - ./gradlew :bottlenote-product-api:build -x test -x asciidoctor --build-cache --parallel - - - name: refresh docker build action from main - run: | - git fetch --no-tags origin '+refs/heads/main:refs/remotes/origin/main' - git checkout origin/main -- .github/actions/docker-build-push/action.yml - - - name: build and push to registry - id: build - uses: ./.github/actions/docker-build-push - with: - registry-url: ${{ secrets.REGISTRY_ADDRESS }} - registry-username: ${{ secrets.REGISTRY_USERNAME }} - registry-password: ${{ secrets.REGISTRY_PASSWORD }} - image-name: ${{ env.IMAGE_NAME }} - image-tag: ${{ needs.verify-product-release.outputs.image-tag }} - additional-tags: product_latest - dockerfile: Dockerfile - context: . - platforms: linux/arm64 - cache-scope: product-production - image-title: "BottleNote Product API" - image-description: "BottleNote Product API Server" - image-vendor: "BottleNote" - image-authors: "BottleNote Team" - sign-image: 'true' - cosign-key-path: 'git.environment-variables/storage/docker-registry/cosign.key' - cosign-password: ${{ secrets.COSIGN_PASSWORD }} - - update-production: - name: update product production image - needs: - - verify-product-release - - build-and-push - runs-on: ubuntu-latest - steps: - - name: checkout code with submodules - uses: actions/checkout@v7 - with: - submodules: true - token: ${{ secrets.GIT_ACCESS_TOKEN }} - - - name: setup kustomize - uses: imranismail/setup-kustomize@v3 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: update production image tag - run: | - IMAGE_TAG="${{ needs.build-and-push.outputs.image-tag }}" - REGISTRY="${{ secrets.REGISTRY_ADDRESS }}" - echo "Updating product production image tag: $IMAGE_TAG" - cd git.environment-variables - git checkout main - cd deploy/overlays/production - kustomize edit set image bottlenote-product-api=${REGISTRY}/${{ env.IMAGE_NAME }}:${IMAGE_TAG} - echo "production kustomization.yaml updated" - - - name: commit and push with retry - run: | - cd git.environment-variables - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add deploy/overlays/production/kustomization.yaml - if git diff --staged --quiet; then - echo "No changes to commit" - exit 0 - fi - git commit -m "chore(product): update production image tag to ${{ needs.build-and-push.outputs.image-tag }} - - Updated by GitHub Actions - Release: ${{ needs.verify-product-release.outputs.release-tag }} - Commit: ${{ needs.verify-product-release.outputs.tag-sha }} - Workflow: ${{ github.workflow }}" - for i in {1..5}; do - git pull --rebase origin main && git push origin main && break - echo "Push failed, retry $i/5..." - sleep $((i * 2)) - done - echo "Product production deployment triggered" diff --git a/.github/workflows/release-draft-gate.yml.bak b/.github/workflows/release-draft-gate.yml.bak deleted file mode 100644 index 74a754e41..000000000 --- a/.github/workflows/release-draft-gate.yml.bak +++ /dev/null @@ -1,160 +0,0 @@ -name: release draft gate - -on: - release: - types: - - created - - published - workflow_dispatch: - inputs: - tag: - description: 'Backend release tag, for example backend/v1.2.3' - required: true - type: string - expected_state: - description: 'Expected release state' - required: true - type: choice - default: draft - options: - - draft - - published - -permissions: - contents: write - -concurrency: - group: release-draft-gate-${{ github.event_name }}-${{ github.event.release.tag_name || inputs.tag }} - cancel-in-progress: false - -jobs: - gate: - name: validate backend release - runs-on: ubuntu-latest - timeout-minutes: 3 - env: - EVENT_NAME: ${{ github.event_name }} - RELEASE_ACTION: ${{ github.event.action || '' }} - INPUT_TAG: ${{ inputs.tag || '' }} - INPUT_EXPECTED_STATE: ${{ inputs.expected_state || '' }} - EVENT_TAG: ${{ github.event.release.tag_name || '' }} - EVENT_DRAFT: ${{ github.event.release.draft || '' }} - EVENT_PRERELEASE: ${{ github.event.release.prerelease || '' }} - steps: - - name: Checkout repository - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Resolve release target - run: | - set -euo pipefail - release_tag="${EVENT_TAG:-$INPUT_TAG}" - [[ -n "$release_tag" ]] || { echo "::error::release tag is required"; exit 1; } - [[ "$release_tag" =~ ^backend/v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::backend release tag required, got: $release_tag"; exit 1; } - - version="${release_tag#backend/v}" - image_tag="backend_${version}" - - expected_state="$INPUT_EXPECTED_STATE" - if [[ -z "$expected_state" ]]; then - case "$RELEASE_ACTION" in - created) expected_state="draft" ;; - published) expected_state="published" ;; - *) expected_state="draft" ;; - esac - fi - - { - echo "RELEASE_TAG=$release_tag" - echo "EXPECTED_STATE=$expected_state" - echo "VERSION=$version" - echo "IMAGE_TAG=$image_tag" - } >> "$GITHUB_ENV" - - echo "[release-draft-gate] event=$EVENT_NAME action=${RELEASE_ACTION:-manual} tag=$release_tag expected_state=$expected_state image_tag=$image_tag" - - - name: Fetch main and release tag - run: | - set -euo pipefail - git fetch --no-tags origin '+refs/heads/main:refs/remotes/origin/main' - git fetch --tags origin - git rev-parse -q --verify "refs/tags/${RELEASE_TAG}" >/dev/null || { echo "::error::release tag not found: $RELEASE_TAG"; exit 1; } - tag_sha="$(git rev-list -n 1 "$RELEASE_TAG")" - echo "TAG_SHA=$tag_sha" >> "$GITHUB_ENV" - echo "[release-draft-gate] tag_sha=$tag_sha" - - - name: Verify tag commit is on main - run: | - set -euo pipefail - git merge-base --is-ancestor "$TAG_SHA" origin/main || { echo "::error::tag commit is not contained in origin/main: $TAG_SHA"; exit 1; } - echo "[release-draft-gate] tag_on_main=true" - - - name: Verify release metadata - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - releases_json="$(curl -fsSL \ - -H "Authorization: Bearer ${GH_TOKEN}" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases?per_page=100")" || { echo "::error::failed to list releases"; exit 1; } - RELEASES_JSON="$releases_json" python3 - <<'PY' - import json, os, sys - tag = os.environ['RELEASE_TAG'] - releases = json.loads(os.environ['RELEASES_JSON']) - data = next((release for release in releases if release.get('tag_name') == tag), None) - if data is None: - print(f'::error::release not found for tag: {tag}', file=sys.stderr) - sys.exit(1) - expected = os.environ['EXPECTED_STATE'] - draft = bool(data.get('draft')) - prerelease = bool(data.get('prerelease')) - body = (data.get('body') or '').strip() - if expected == 'draft' and not draft: - print('::error::release must be draft for draft gate', file=sys.stderr) - sys.exit(1) - if expected == 'published' and draft: - print('::error::release must be published after publish event', file=sys.stderr) - sys.exit(1) - if prerelease: - print('::error::production release must not be marked as prerelease', file=sys.stderr) - sys.exit(1) - if len(body) < 40: - print('::error::release changelog is empty or too short', file=sys.stderr) - sys.exit(1) - with open(os.environ['GITHUB_ENV'], 'a', encoding='utf-8') as env: - env.write(f"RELEASE_ID={data.get('id')}\n") - print(f"[release-draft-gate] release_state={expected} release_id={data.get('id')} body_length={len(body)}") - PY - - - name: Publish draft release - if: ${{ env.EXPECTED_STATE == 'draft' }} - env: - PUBLISH_TOKEN: ${{ secrets.GIT_ACCESS_TOKEN }} - run: | - set -euo pipefail - [[ -n "${PUBLISH_TOKEN:-}" ]] || { echo "::error::GIT_ACCESS_TOKEN is required so release.published workflows are triggered"; exit 1; } - curl -fsSL -X PATCH \ - -H "Authorization: Bearer ${PUBLISH_TOKEN}" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" \ - -d '{"draft":false}' >/dev/null - echo "[release-draft-gate] backend release published by approved workflow" - - - name: Write release gate summary - run: | - set -euo pipefail - { - echo "## Backend Release Gate" - echo "- Event: \`${EVENT_NAME}/${RELEASE_ACTION:-manual}\`" - echo "- Release tag: \`${RELEASE_TAG}\`" - echo "- Expected state: \`${EXPECTED_STATE}\`" - echo "- Tag commit: \`${TAG_SHA}\`" - echo "- Image tag: \`${IMAGE_TAG}\`" - echo "- Main ancestry: \`passed\`" - echo "- Release metadata: \`passed\`" - } >> "$GITHUB_STEP_SUMMARY" - echo "[release-draft-gate] release gate passed" diff --git a/.gitignore b/.gitignore index 793e1b400..48f272f72 100644 --- a/.gitignore +++ b/.gitignore @@ -5,11 +5,6 @@ build/ !**/src/test/**/build/ docs/product-api.html -### Production application.yml ### -src/main/resources/application-dev.yml -src/main/resources/application-prod.yml -src/main/resources/secrets/monitoring.yml - ### STS ### .apt_generated .classpath @@ -42,12 +37,6 @@ out/ .vscode/ /.env -# Ignore Gradle build output directory -build -/src/test/resources/application-dev.yml -/src/main/resources/fcm/service_account_credentials.json -/src/main/resources/META-INF/additional-spring-configuration-metadata.json -/docs/product-api.html /logs/ # QueryDSL generated files **/src/main/generated/ @@ -55,6 +44,7 @@ build /.secrets CLAUDE.personal.md +AGENTS.personal.md # macOS .DS_Store @@ -62,7 +52,6 @@ CLAUDE.personal.md # .env files .env /prod.env -http_old/ settings.local.json # Cosign keys @@ -86,9 +75,6 @@ docs/_site/ # Local static demo workspace .example/ -# 기존 Jekyll 빌드 결과물 (Antora 전환 후 삭제 예정) -docs/admin-api.html -docs/index.html */settings.local.json /tmp/* diff --git a/AGENTS.md b/AGENTS.md index 95862999c..417418d40 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,8 @@ # AGENTS.md -This file provides guidance to Codex (Codex.ai/code) when working with code in this repository. +This file provides guidance to Codex CLI when working with code in this repository. -> If a `Codex.personal.md` file exists in the same directory, also refer to its contents. +> If a `AGENTS.personal.md` file exists in the same directory, also refer to its contents. > **Team instructions take priority over personal instructions when they conflict.** ## User Instructions @@ -66,9 +66,34 @@ git submodule update --init --recursive ### 서브모듈 -- **git.environment-variables**: 환경 설정 및 초기화 스크립트 포함 - - `storage/mysql/init/*.sql`: TestContainers용 DB 초기화 스크립트 - - 통합 테스트 실행 전 서브모듈 초기화 필수 +- **git.environment-variables**: 환경 설정과 DB 마이그레이션 SQL을 담은 비공개 서브모듈 + - `storage/db/migration/*.sql`: Flyway 마이그레이션 원본 + - `storage/mysql/sql/*.sql`: batch 전용 쿼리 리소스 + - 빌드와 통합 테스트 실행 전 서브모듈 초기화 필수 + +## 배포 및 실행 인프라 + +- 운영·개발 모두 Kubernetes에서 돌아가고 GitOps로 배포한다. k8s 매니페스트는 이 저장소가 아니라 별도 저장소에 있다. +- **애플리케이션 인스턴스는 다중이다.** 요청 카운팅, 분산 잠금, 중복 억제, 스케줄 단일 실행 같은 것을 JVM 로컬 상태(static 필드, 인메모리 캐시)로 구현하면 안 된다. Redis 등 공유 저장소를 쓴다. +- 앞단 게이트웨이가 클라이언트를 통해 들어온 `X-Forwarded-For`를 제거하고 실제 접속 주소로 다시 채운다. 따라서 앱이 받는 XFF는 신뢰할 수 있다. + +### 배포 방법 + +배포는 모두 GitHub Actions로 수행한다. **로컬에서 이미지 빌드·푸시·매니페스트 수정을 직접 하지 않는다.** + +| 대상 | 워크플로 | 트리거 | +|------|---------|--------| +| product-api / admin-api (운영) | `deploy_release_applications.yml` | `backend/vX.Y.Z` 릴리스 published | +| product-api / admin-api (개발) | `deploy_development_applications.yml` | main CI 성공 시 자동, 또는 수동 dispatch | +| batch | `deploy_batch.yml` | 수동 dispatch (`environment`, `version` 입력) | + +```bash +# batch 배포 — version은 정확한 X.Y.Z, production은 main에서만 허용 +gh workflow run deploy_batch.yml -f environment=production -f version=1.2.3 + +# 개발 환경 수동 배포 +gh workflow run deploy_development_applications.yml +``` ## Skills (Development Workflow) @@ -91,6 +116,20 @@ Use these skills to follow the structured development lifecycle: - `.agents/skills/implement/references/languages/bottlenote-patterns.md` — 프로젝트 특화 패턴 (InMemory 갱신 체크리스트 등) - `.agents/skills/test/references/testing/java.md` — Java 테스트 패턴 +## 에이전트 설정 미러 규칙 + +이 저장소는 codex와 Claude Code용 설정을 이중으로 보관한다. **한쪽을 고치면 반드시 다른 쪽도 같이 고친다.** + +| 대상 | codex | Claude Code | +|---|---|---| +| 지침 | `AGENTS.md` | `CLAUDE.md` | +| 스킬 | `.agents` 디렉터리 | `.claude` 디렉터리 | + +- 두 지침 문서는 런타임 이름, 스킬 디렉터리 경로, personal 지침 파일명, 첫 줄 제목만 다르고 나머지 본문은 완전히 같아야 한다. 이 네 가지 외의 차이는 동기화 누락이다. +- 두 스킬 디렉터리는 바이트 단위로 같아야 한다. `diff -rq`로 확인하고, 스킬 파일 안에 특정 런타임의 디렉터리 경로를 하드코딩하지 않는다. +- Skills 표에 적은 슬래시 커맨드는 실제 스킬 디렉터리로 존재해야 한다. 스킬을 제거하면 표에서도 지운다. +- 훅을 다시 도입한다면 머신 고유 절대경로와 런타임 전용 환경변수를 넣지 않는다. 저장소 루트는 `$(git rev-parse --show-toplevel)`로 구한다. + ## 코드 작성 규칙 ### 아키텍처 패턴 @@ -98,6 +137,8 @@ Use these skills to follow the structured development lifecycle: - **계층 구조**: Controller → Facade <-> Service → Repository → Domain - **도메인별 패키지**: constant, controller, domain, dto, repository, service, facade, exception, event +**설계 배경 — Facade는 도메인 간 완충 지대다.** 여러 도메인이 양방향으로 통신하는 구조(user↔alcohols 등)에서 Service끼리 직접 상호 참조하면 순환 의존과 부채가 쌓인다. 그래서 타 도메인 접근은 상대의 Facade 인터페이스로만 하며, 결합을 좁고 보이는 한 지점으로 모아 관리한다. 배치 상세는 아래 `@FacadeService` 항목, 강제 수단은 ArchUnit(`./gradlew check_rule_test`)이다. + ### 레이어 표준 15 (2026-06-10 확정) 모듈 기술 부채 정리의 기준이 되는 표준. ArchUnit `@Tag("rule")` 테스트로 강제하며, 위반 0이 된 룰부터 활성화한다. @@ -125,37 +166,15 @@ Use these skills to follow the structured development lifecycle: ### 프로젝트 특화 어노테이션 -#### 계층별 어노테이션 - -**@FacadeService** -- **역할**: 퍼사드 서비스 계층 표시 -- **위치**: `app.bottlenote.{domain}.facade` -- **특징**: `@Service` 포함, 스프링 컴포넌트로 자동 등록 -- **용도**: 여러 서비스를 조합하는 퍼사드 패턴 구현 - -**@DomainRepository** -- **역할**: 순수 도메인 레포지토리 인터페이스 표시 -- **위치**: `app.bottlenote.{domain}.domain` -- **특징**: 프레임워크 독립적, Spring/JPA에 의존하지 않음 -- **용도**: 도메인이 할 수 있는 행위를 정의하는 순수 비즈니스 인터페이스 - -**@JpaRepositoryImpl** -- **역할**: JPA 레포지토리 구현체 표시 -- **위치**: `app.bottlenote.{domain}.repository` -- **특징**: `@Repository` 포함, 영속성 예외 변환 제공 -- **용도**: 도메인 레포지토리의 실제 데이터베이스 접근 구현 - -**@DomainEventListener** -- **역할**: 도메인 이벤트 리스너 표시 -- **위치**: `app.bottlenote.{domain}.event` -- **특징**: `@Component` 포함, 동기/비동기 처리 방식 지정 가능 (`ProcessingType`) -- **용도**: 도메인 이벤트를 처리하는 리스너 구현 - -**@ThirdPartyService** -- **역할**: 외부 서비스 연동 계층 표시 -- **위치**: `app.external` 또는 관련 패키지 -- **특징**: `@Service` 포함, 트랜잭션 불필요 -- **용도**: AWS, 외부 API 등 써드파티 시스템 통신 +| 어노테이션 | 위치 | 포함 | 용도 | +|---|---|---|---| +| `@FacadeService` | 구현체 `Default{도메인명}Facade`는 `{domain}.service`, 인터페이스는 `{domain}.facade`(공개 계약) | `@Service` | 도메인 간 완충 계층 구현체. 타 도메인은 Facade 인터페이스만 호출한다 | +| `@DomainRepository` | `{domain}.domain` | 없음 (마커) | Spring/JPA 비의존 도메인 레포지토리 인터페이스 | +| `@JpaRepositoryImpl` | `{domain}.repository` | `@Repository` | 도메인 레포지토리의 JPA 구현체, 영속성 예외 변환 | +| `@DomainEventListener` | `{domain}.event` | `@Component` | 도메인 이벤트 리스너, `ProcessingType`으로 동기/비동기 지정 | +| `@ThirdPartyService` | `app.external` | `@Service` | AWS·외부 API 등 써드파티 연동 계층 | + +> 코드 예시: `.agents/skills/implement/references/languages/java-spring.md` ### 예외 처리 @@ -207,6 +226,16 @@ Use these skills to follow the structured development lifecycle: ## 데이터베이스 설계 +### 스키마 마이그레이션 (Flyway) + +스키마는 Flyway가 애플리케이션 기동 시점에 적용한다. `ddl-auto`는 `validate`이며 Hibernate가 스키마를 생성하거나 변경하지 않는다. 엔티티를 바꾸면 마이그레이션도 함께 작성해야 하고, 그러지 않으면 기동 시 validate에서 실패한다. + +- 마이그레이션 원본은 `git.environment-variables/storage/db/migration/`에 두고, product-api와 admin-api의 `processResources`가 빌드 시 `classpath:db/migration`으로 복사한다. 저장소 소스 트리에는 `db/migration` 디렉터리가 없다. +- 설정은 `enabled: ${FLYWAY_ENABLED:true}`, `baseline-on-migrate: false`, `locations: classpath:db/migration`이다. 기존 스키마에 임의로 baseline을 잡지 않는다. +- batch 모듈은 `flyway.enabled=false`다. 마이그레이션 주체가 아니며, batch jar에 마이그레이션 SQL이 섞여 들어가면 빌드가 실패하는 가드가 있다. batch는 `storage/mysql/sql`의 지정된 쿼리 리소스만 패키징한다. +- 통합 테스트도 `flyway.enabled=true`로 동작한다. TestContainers 스키마 역시 Flyway가 만들며, 별도 init 스크립트를 쓰지 않는다. +- 새 마이그레이션은 서브모듈 저장소에 추가한 뒤 서브모듈 포인터를 갱신해야 반영된다. + ### JPA 엔티티 - `BaseEntity` 상속 (공통 필드) @@ -215,54 +244,13 @@ Use these skills to follow the structured development lifecycle: ### 레포지토리 계층 구조 -#### 1. 도메인 레포지토리 (필수) -- **위치**: `app.bottlenote.{domain}.domain` -- **네이밍**: `{도메인명}Repository` -- **역할**: 해당 도메인이 할 수 있는 행위를 정의만 하는 순수 비즈니스 인터페이스 -- **어노테이션**: - - `@DomainRepository` (선택) - 도메인 레포지토리임을 명시적으로 표시 - - 어노테이션 없이 순수 인터페이스로만 작성 가능 -- **원칙**: - - Spring, JPA에 의존하지 않음 - - 도메인 계층에 위치 - - 서비스 계층은 이 인터페이스에만 의존 - -#### 2. JPA 레포지토리 (필수) -- **위치**: `app.bottlenote.{domain}.repository` -- **네이밍**: `Jpa{도메인명}Repository` -- **역할**: 도메인 레포지토리의 실제 데이터베이스 접근 구현체 -- **어노테이션**: - - `@JpaRepositoryImpl` (필수) - JPA 구현체임을 표시하고 `@Repository` 기능 제공 - - 영속성 예외를 Spring의 DataAccessException으로 자동 변환 -- **원칙**: - - `JpaRepository` 상속으로 기본 CRUD 제공 - - 도메인 레포지토리 인터페이스 구현 - - 단순 조회는 메서드 쿼리 또는 `@Query` JPQL 사용 - - QueryDSL Custom 레포지토리 통합 (필요 시) - -#### 3. QueryDSL 레포지토리 (선택 - 복잡한 쿼리만) -- **역할**: 복잡한 동적 쿼리를 타입 세이프하게 작성하기 위한 확장 레포지토리 -- **사용 시점**: 메서드 쿼리나 JPQL로 표현하기 어려운 복잡한 쿼리가 필요할 때만 사용 - -**구성 요소**: -- **Custom 인터페이스**: `Custom{도메인명}Repository` (위치: repository 패키지) - - 어노테이션 불필요 (순수 인터페이스) -- **구현체**: `Custom{도메인명}RepositoryImpl` (위치: repository 패키지) - - 어노테이션 불필요 (Spring Data JPA가 자동 감지) -- **쿼리 서포터**: `{도메인명}QuerySupporter` (위치: repository 패키지) - - `@Component` (필수) - 재사용 로직을 제공하는 스프링 빈 - -**QueryDSL 사용 기준**: -- ✅ 복잡한 동적 조건 (여러 필터 조합) -- ✅ 다중 테이블 조인 및 집계 -- ✅ 복잡한 Projection (DTO 변환) -- ❌ 단순 CRUD -- ❌ 단일 조건 조회 (메서드 쿼리 사용) - -**성능 최적화**: -- 페치 조인, `@BatchSize` 활용 (N+1 방지) -- `@Cacheable` 적절히 사용 -- 불필요한 컬럼 조회 방지 (Projection 활용) +레이어 표준 2·4가 원칙이다: 포트(순수 인터페이스)와 구현을 분리하고, 기술 세부사항은 구현 안에 격리한다. + +1. **도메인 레포지토리** (필수): `{도메인명}Repository`, `{domain}.domain` 위치, `@DomainRepository`는 선택. Spring/JPA 비의존 순수 인터페이스이며 Service는 여기에만 의존한다. +2. **JPA 레포지토리** (필수): `Jpa{도메인명}Repository`, `{domain}.repository` 위치, `@JpaRepositoryImpl` 필수. `JpaRepository` 상속 + 도메인 레포지토리 구현. 단순 조회는 메서드 쿼리 또는 `@Query` JPQL로 해결한다. +3. **QueryDSL 레포지토리** (복잡한 쿼리만): `Custom{도메인명}Repository` / `Custom{도메인명}RepositoryImpl` / `{도메인명}QuerySupporter`(`@Component`), 전부 repository 패키지. 동적 조건 조합·다중 조인·복잡한 Projection에만 쓰고, 단순 CRUD나 단일 조건 조회에는 쓰지 않는다. + +> 구현 예시: `.agents/skills/implement/references/languages/java-spring.md` ## 보안 및 인증 @@ -275,38 +263,9 @@ Use these skills to follow the structured development lifecycle: - OpenFeign: `@FeignClient`, 설정 분리 `FeignConfig`, 에러 처리 `ErrorDecoder` - AWS S3: PreSigned URL 생성 (SDK v2 `S3Client`/`S3Presigner`) -## 좋은 Spring Boot 개발 관습 - -### 응답 통일성 - -- API 응답 형식 통일: `GlobalResponse` 또는 `ResponseEntity` 일관성 유지 -- 에러 응답 표준화: HTTP 상태 코드와 에러 메시지 일관성 - -### 성능 최적화 - -- N+1 문제 방지: 페치 조인, `@BatchSize`, 쿼리 최적화 -- 캐싱 전략: `@Cacheable` 적절히 활용 -- 비동기 처리: `@Async`, 이벤트 기반 처리 - -### 보안 기본 원칙 - -- 입력값 검증: `@Valid`, `@Validated` 사용 -- 민감 정보 로깅 금지 -- SQL 인젝션 방지: PreparedStatement 사용 - -### 테스트 품질 - -- 단위 테스트와 통합 테스트 분리 -- 테스트 데이터 격리: 각 테스트 독립성 보장 -- Mock 대신 Fake/InMemory 구현으로 외부 의존성 분리 (레이어 표준 10) - -### 코드 품질 - -- 의존성 주입: 생성자 주입 우선 -- 불변성 지향: `final` 필드, `record` 활용 -- 단일 책임 원칙: 클래스와 메서드 역할 명확화 +## GSL Runtime Boundary Rules -## GSL on codex — Runtime Boundary Rules +> [상태] 현재 GSL 스킬셋은 구형이다. 별도 세션에서 개편할 예정이며, 개편 전까지 아래 경계 규칙을 유지한다. When using GSL skills from `.agents/skills/`, treat each `SKILL.md` as a single-turn procedure. diff --git a/CLAUDE.md b/CLAUDE.md index 63940e66e..60dccd88a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to Claude Code when working with code in this repository. > If a `CLAUDE.personal.md` file exists in the same directory, also refer to its contents. > **Team instructions take priority over personal instructions when they conflict.** @@ -66,9 +66,34 @@ git submodule update --init --recursive ### 서브모듈 -- **git.environment-variables**: 환경 설정 및 초기화 스크립트 포함 - - `storage/mysql/init/*.sql`: TestContainers용 DB 초기화 스크립트 - - 통합 테스트 실행 전 서브모듈 초기화 필수 +- **git.environment-variables**: 환경 설정과 DB 마이그레이션 SQL을 담은 비공개 서브모듈 + - `storage/db/migration/*.sql`: Flyway 마이그레이션 원본 + - `storage/mysql/sql/*.sql`: batch 전용 쿼리 리소스 + - 빌드와 통합 테스트 실행 전 서브모듈 초기화 필수 + +## 배포 및 실행 인프라 + +- 운영·개발 모두 Kubernetes에서 돌아가고 GitOps로 배포한다. k8s 매니페스트는 이 저장소가 아니라 별도 저장소에 있다. +- **애플리케이션 인스턴스는 다중이다.** 요청 카운팅, 분산 잠금, 중복 억제, 스케줄 단일 실행 같은 것을 JVM 로컬 상태(static 필드, 인메모리 캐시)로 구현하면 안 된다. Redis 등 공유 저장소를 쓴다. +- 앞단 게이트웨이가 클라이언트를 통해 들어온 `X-Forwarded-For`를 제거하고 실제 접속 주소로 다시 채운다. 따라서 앱이 받는 XFF는 신뢰할 수 있다. + +### 배포 방법 + +배포는 모두 GitHub Actions로 수행한다. **로컬에서 이미지 빌드·푸시·매니페스트 수정을 직접 하지 않는다.** + +| 대상 | 워크플로 | 트리거 | +|------|---------|--------| +| product-api / admin-api (운영) | `deploy_release_applications.yml` | `backend/vX.Y.Z` 릴리스 published | +| product-api / admin-api (개발) | `deploy_development_applications.yml` | main CI 성공 시 자동, 또는 수동 dispatch | +| batch | `deploy_batch.yml` | 수동 dispatch (`environment`, `version` 입력) | + +```bash +# batch 배포 — version은 정확한 X.Y.Z, production은 main에서만 허용 +gh workflow run deploy_batch.yml -f environment=production -f version=1.2.3 + +# 개발 환경 수동 배포 +gh workflow run deploy_development_applications.yml +``` ## Skills (Development Workflow) @@ -91,6 +116,20 @@ Use these skills to follow the structured development lifecycle: - `.claude/skills/implement/references/languages/bottlenote-patterns.md` — 프로젝트 특화 패턴 (InMemory 갱신 체크리스트 등) - `.claude/skills/test/references/testing/java.md` — Java 테스트 패턴 +## 에이전트 설정 미러 규칙 + +이 저장소는 codex와 Claude Code용 설정을 이중으로 보관한다. **한쪽을 고치면 반드시 다른 쪽도 같이 고친다.** + +| 대상 | codex | Claude Code | +|---|---|---| +| 지침 | `AGENTS.md` | `CLAUDE.md` | +| 스킬 | `.agents` 디렉터리 | `.claude` 디렉터리 | + +- 두 지침 문서는 런타임 이름, 스킬 디렉터리 경로, personal 지침 파일명, 첫 줄 제목만 다르고 나머지 본문은 완전히 같아야 한다. 이 네 가지 외의 차이는 동기화 누락이다. +- 두 스킬 디렉터리는 바이트 단위로 같아야 한다. `diff -rq`로 확인하고, 스킬 파일 안에 특정 런타임의 디렉터리 경로를 하드코딩하지 않는다. +- Skills 표에 적은 슬래시 커맨드는 실제 스킬 디렉터리로 존재해야 한다. 스킬을 제거하면 표에서도 지운다. +- 훅을 다시 도입한다면 머신 고유 절대경로와 런타임 전용 환경변수를 넣지 않는다. 저장소 루트는 `$(git rev-parse --show-toplevel)`로 구한다. + ## 코드 작성 규칙 ### 아키텍처 패턴 @@ -98,6 +137,8 @@ Use these skills to follow the structured development lifecycle: - **계층 구조**: Controller → Facade <-> Service → Repository → Domain - **도메인별 패키지**: constant, controller, domain, dto, repository, service, facade, exception, event +**설계 배경 — Facade는 도메인 간 완충 지대다.** 여러 도메인이 양방향으로 통신하는 구조(user↔alcohols 등)에서 Service끼리 직접 상호 참조하면 순환 의존과 부채가 쌓인다. 그래서 타 도메인 접근은 상대의 Facade 인터페이스로만 하며, 결합을 좁고 보이는 한 지점으로 모아 관리한다. 배치 상세는 아래 `@FacadeService` 항목, 강제 수단은 ArchUnit(`./gradlew check_rule_test`)이다. + ### 레이어 표준 15 (2026-06-10 확정) 모듈 기술 부채 정리의 기준이 되는 표준. ArchUnit `@Tag("rule")` 테스트로 강제하며, 위반 0이 된 룰부터 활성화한다. @@ -125,37 +166,15 @@ Use these skills to follow the structured development lifecycle: ### 프로젝트 특화 어노테이션 -#### 계층별 어노테이션 - -**@FacadeService** -- **역할**: 퍼사드 서비스 계층 표시 -- **위치**: `app.bottlenote.{domain}.facade` -- **특징**: `@Service` 포함, 스프링 컴포넌트로 자동 등록 -- **용도**: 여러 서비스를 조합하는 퍼사드 패턴 구현 - -**@DomainRepository** -- **역할**: 순수 도메인 레포지토리 인터페이스 표시 -- **위치**: `app.bottlenote.{domain}.domain` -- **특징**: 프레임워크 독립적, Spring/JPA에 의존하지 않음 -- **용도**: 도메인이 할 수 있는 행위를 정의하는 순수 비즈니스 인터페이스 - -**@JpaRepositoryImpl** -- **역할**: JPA 레포지토리 구현체 표시 -- **위치**: `app.bottlenote.{domain}.repository` -- **특징**: `@Repository` 포함, 영속성 예외 변환 제공 -- **용도**: 도메인 레포지토리의 실제 데이터베이스 접근 구현 - -**@DomainEventListener** -- **역할**: 도메인 이벤트 리스너 표시 -- **위치**: `app.bottlenote.{domain}.event` -- **특징**: `@Component` 포함, 동기/비동기 처리 방식 지정 가능 (`ProcessingType`) -- **용도**: 도메인 이벤트를 처리하는 리스너 구현 - -**@ThirdPartyService** -- **역할**: 외부 서비스 연동 계층 표시 -- **위치**: `app.external` 또는 관련 패키지 -- **특징**: `@Service` 포함, 트랜잭션 불필요 -- **용도**: AWS, 외부 API 등 써드파티 시스템 통신 +| 어노테이션 | 위치 | 포함 | 용도 | +|---|---|---|---| +| `@FacadeService` | 구현체 `Default{도메인명}Facade`는 `{domain}.service`, 인터페이스는 `{domain}.facade`(공개 계약) | `@Service` | 도메인 간 완충 계층 구현체. 타 도메인은 Facade 인터페이스만 호출한다 | +| `@DomainRepository` | `{domain}.domain` | 없음 (마커) | Spring/JPA 비의존 도메인 레포지토리 인터페이스 | +| `@JpaRepositoryImpl` | `{domain}.repository` | `@Repository` | 도메인 레포지토리의 JPA 구현체, 영속성 예외 변환 | +| `@DomainEventListener` | `{domain}.event` | `@Component` | 도메인 이벤트 리스너, `ProcessingType`으로 동기/비동기 지정 | +| `@ThirdPartyService` | `app.external` | `@Service` | AWS·외부 API 등 써드파티 연동 계층 | + +> 코드 예시: `.claude/skills/implement/references/languages/java-spring.md` ### 예외 처리 @@ -207,6 +226,16 @@ Use these skills to follow the structured development lifecycle: ## 데이터베이스 설계 +### 스키마 마이그레이션 (Flyway) + +스키마는 Flyway가 애플리케이션 기동 시점에 적용한다. `ddl-auto`는 `validate`이며 Hibernate가 스키마를 생성하거나 변경하지 않는다. 엔티티를 바꾸면 마이그레이션도 함께 작성해야 하고, 그러지 않으면 기동 시 validate에서 실패한다. + +- 마이그레이션 원본은 `git.environment-variables/storage/db/migration/`에 두고, product-api와 admin-api의 `processResources`가 빌드 시 `classpath:db/migration`으로 복사한다. 저장소 소스 트리에는 `db/migration` 디렉터리가 없다. +- 설정은 `enabled: ${FLYWAY_ENABLED:true}`, `baseline-on-migrate: false`, `locations: classpath:db/migration`이다. 기존 스키마에 임의로 baseline을 잡지 않는다. +- batch 모듈은 `flyway.enabled=false`다. 마이그레이션 주체가 아니며, batch jar에 마이그레이션 SQL이 섞여 들어가면 빌드가 실패하는 가드가 있다. batch는 `storage/mysql/sql`의 지정된 쿼리 리소스만 패키징한다. +- 통합 테스트도 `flyway.enabled=true`로 동작한다. TestContainers 스키마 역시 Flyway가 만들며, 별도 init 스크립트를 쓰지 않는다. +- 새 마이그레이션은 서브모듈 저장소에 추가한 뒤 서브모듈 포인터를 갱신해야 반영된다. + ### JPA 엔티티 - `BaseEntity` 상속 (공통 필드) @@ -215,54 +244,13 @@ Use these skills to follow the structured development lifecycle: ### 레포지토리 계층 구조 -#### 1. 도메인 레포지토리 (필수) -- **위치**: `app.bottlenote.{domain}.domain` -- **네이밍**: `{도메인명}Repository` -- **역할**: 해당 도메인이 할 수 있는 행위를 정의만 하는 순수 비즈니스 인터페이스 -- **어노테이션**: - - `@DomainRepository` (선택) - 도메인 레포지토리임을 명시적으로 표시 - - 어노테이션 없이 순수 인터페이스로만 작성 가능 -- **원칙**: - - Spring, JPA에 의존하지 않음 - - 도메인 계층에 위치 - - 서비스 계층은 이 인터페이스에만 의존 - -#### 2. JPA 레포지토리 (필수) -- **위치**: `app.bottlenote.{domain}.repository` -- **네이밍**: `Jpa{도메인명}Repository` -- **역할**: 도메인 레포지토리의 실제 데이터베이스 접근 구현체 -- **어노테이션**: - - `@JpaRepositoryImpl` (필수) - JPA 구현체임을 표시하고 `@Repository` 기능 제공 - - 영속성 예외를 Spring의 DataAccessException으로 자동 변환 -- **원칙**: - - `JpaRepository` 상속으로 기본 CRUD 제공 - - 도메인 레포지토리 인터페이스 구현 - - 단순 조회는 메서드 쿼리 또는 `@Query` JPQL 사용 - - QueryDSL Custom 레포지토리 통합 (필요 시) - -#### 3. QueryDSL 레포지토리 (선택 - 복잡한 쿼리만) -- **역할**: 복잡한 동적 쿼리를 타입 세이프하게 작성하기 위한 확장 레포지토리 -- **사용 시점**: 메서드 쿼리나 JPQL로 표현하기 어려운 복잡한 쿼리가 필요할 때만 사용 - -**구성 요소**: -- **Custom 인터페이스**: `Custom{도메인명}Repository` (위치: repository 패키지) - - 어노테이션 불필요 (순수 인터페이스) -- **구현체**: `Custom{도메인명}RepositoryImpl` (위치: repository 패키지) - - 어노테이션 불필요 (Spring Data JPA가 자동 감지) -- **쿼리 서포터**: `{도메인명}QuerySupporter` (위치: repository 패키지) - - `@Component` (필수) - 재사용 로직을 제공하는 스프링 빈 - -**QueryDSL 사용 기준**: -- ✅ 복잡한 동적 조건 (여러 필터 조합) -- ✅ 다중 테이블 조인 및 집계 -- ✅ 복잡한 Projection (DTO 변환) -- ❌ 단순 CRUD -- ❌ 단일 조건 조회 (메서드 쿼리 사용) - -**성능 최적화**: -- 페치 조인, `@BatchSize` 활용 (N+1 방지) -- `@Cacheable` 적절히 사용 -- 불필요한 컬럼 조회 방지 (Projection 활용) +레이어 표준 2·4가 원칙이다: 포트(순수 인터페이스)와 구현을 분리하고, 기술 세부사항은 구현 안에 격리한다. + +1. **도메인 레포지토리** (필수): `{도메인명}Repository`, `{domain}.domain` 위치, `@DomainRepository`는 선택. Spring/JPA 비의존 순수 인터페이스이며 Service는 여기에만 의존한다. +2. **JPA 레포지토리** (필수): `Jpa{도메인명}Repository`, `{domain}.repository` 위치, `@JpaRepositoryImpl` 필수. `JpaRepository` 상속 + 도메인 레포지토리 구현. 단순 조회는 메서드 쿼리 또는 `@Query` JPQL로 해결한다. +3. **QueryDSL 레포지토리** (복잡한 쿼리만): `Custom{도메인명}Repository` / `Custom{도메인명}RepositoryImpl` / `{도메인명}QuerySupporter`(`@Component`), 전부 repository 패키지. 동적 조건 조합·다중 조인·복잡한 Projection에만 쓰고, 단순 CRUD나 단일 조건 조회에는 쓰지 않는다. + +> 구현 예시: `.claude/skills/implement/references/languages/java-spring.md` ## 보안 및 인증 @@ -275,33 +263,16 @@ Use these skills to follow the structured development lifecycle: - OpenFeign: `@FeignClient`, 설정 분리 `FeignConfig`, 에러 처리 `ErrorDecoder` - AWS S3: PreSigned URL 생성 (SDK v2 `S3Client`/`S3Presigner`) -## 좋은 Spring Boot 개발 관습 - -### 응답 통일성 - -- API 응답 형식 통일: `GlobalResponse` 또는 `ResponseEntity` 일관성 유지 -- 에러 응답 표준화: HTTP 상태 코드와 에러 메시지 일관성 - -### 성능 최적화 - -- N+1 문제 방지: 페치 조인, `@BatchSize`, 쿼리 최적화 -- 캐싱 전략: `@Cacheable` 적절히 활용 -- 비동기 처리: `@Async`, 이벤트 기반 처리 - -### 보안 기본 원칙 - -- 입력값 검증: `@Valid`, `@Validated` 사용 -- 민감 정보 로깅 금지 -- SQL 인젝션 방지: PreparedStatement 사용 - -### 테스트 품질 +## GSL Runtime Boundary Rules -- 단위 테스트와 통합 테스트 분리 -- 테스트 데이터 격리: 각 테스트 독립성 보장 -- Mock 대신 Fake/InMemory 구현으로 외부 의존성 분리 (레이어 표준 10) +> [상태] 현재 GSL 스킬셋은 구형이다. 별도 세션에서 개편할 예정이며, 개편 전까지 아래 경계 규칙을 유지한다. -### 코드 품질 +When using GSL skills from `.claude/skills/`, treat each `SKILL.md` as a single-turn procedure. -- 의존성 주입: 생성자 주입 우선 -- 불변성 지향: `final` 필드, `record` 활용 -- 단일 책임 원칙: 클래스와 메서드 역할 명확화 +- Load only the one GSL skill that matches the user's current explicit request. +- Do not pre-load all 9 GSL skills unless the user explicitly asks for a read-only diagnosis of the skillset. +- A GSL skill boundary is a hard stop. When a skill reaches its Verification / Runtime Boundary section, end the assistant turn. +- `After this skill`, `Next: /...`, or `should invoke` means "suggest the next command", not "execute it now". +- Do not transition between GSL skills (`/define`->`/plan`, `/plan`->`/implement`, `/implement` Task N->N+1, `/implement`->`/test`, `/test`->`/verify`, etc.) in the same assistant turn unless the user explicitly named that next skill — or named multiple Tasks for continuous execution — in their message. +- `/implement` may write test code alongside a Task, but must NOT run the full `/test` / `/verify` / `/self-review` workflow inside itself. Those are separate skill boundaries. +- If the user says "continue" ambiguously, ask whether to run the next suggested GSL skill or only report the next command. diff --git a/README.md b/README.md index 41b5719fb..69a06bcd6 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,23 @@ --- +## 아키텍처 + +### 도메인 간 통신과 Facade 완충 지대 + +이 서버는 alcohols, user, review, rating 등 여러 도메인이 서로를 참조하는 DDD 기반 모듈러 모놀리스입니다. 유저 조회에 위스키 정보가 필요하고, 위스키 조회에 유저 정보가 필요한 양방향 호출이 자연스럽게 발생하는 구조입니다. + +이때 각 도메인의 Service가 상대 도메인의 Service를 직접 참조하게 두면 순환 의존이 생기고, 어떤 메서드가 공개 계약이고 어떤 것이 내부 구현인지 경계가 사라져 변경 비용이 누적됩니다. 그래서 도메인 간 호출을 서로 직접 잇지 않고, 그 사이를 유연하게 완충하는 계층으로 Facade를 두었습니다. + +- `{domain}/facade/` — 타 도메인에 공개하는 유일한 계약. 인터페이스와 전용 payload DTO만 두며, 엔티티는 경계를 넘지 않습니다. +- `{domain}/service/Default{Domain}Facade` — 계약의 구현체. 도메인 내부 사정이므로 밖에서 알 필요가 없습니다. +- 동기 조회는 Facade로, 부수효과 전파(히스토리, 알림)는 도메인 이벤트로 분리해 완충 지대가 병목이 되지 않게 합니다. +- 이 규칙은 문서가 아니라 ArchUnit 아키텍처 테스트(`./gradlew check_rule_test`)가 빌드에서 강제합니다. + +결합을 없애는 설계가 아니라, 결합을 좁고 눈에 보이는 지점으로 모아 관리하는 설계입니다. + +--- + 보틀노트는 위스키 애호가들이 모여 서로의 의견을 공유하고
더욱 즐겁고 유익한 시음 경험을 할 수 있도록 도와줍니다. diff --git a/compose/admin-api.dev.yml b/compose/admin-api.dev.yml deleted file mode 100644 index b6c0bef83..000000000 --- a/compose/admin-api.dev.yml +++ /dev/null @@ -1,32 +0,0 @@ -# ⚠️ 검증 필요: 실제 개발 환경에 맞게 설정값 확인 후 사용하세요 -# - 포트 번호 -# - 메모리 제한 -# - 환경변수 파일 경로 -# - 컨테이너 이름 -# - 어드민 API 서버가 필요한지 확인 - -version: '3.8' - -services: - api: - image: ${FULL_IMAGE_NAME} - container_name: bottle-note-admin-api-development - ports: - - "30002:30002" - env_file: - - .bottlenote.admin.development.ENV - environment: - TZ: Asia/Seoul - SPRING_PROFILES_ACTIVE: dev - JAVA_OPTS: "-Xms256m -Xmx512m -XX:MaxMetaspaceSize=128m" - SERVER_NAME: oracle-node-1-admin - GIT_BRANCH: ${GIT_BRANCH} - GIT_COMMIT: ${GIT_COMMIT} - BUILD_TIME: ${BUILD_TIME} - volumes: - - /var/log/bottle-note:/app/logs - deploy: - resources: - limits: - memory: 768m - restart: unless-stopped diff --git a/compose/admin-api.prod.yml b/compose/admin-api.prod.yml deleted file mode 100644 index 33f726233..000000000 --- a/compose/admin-api.prod.yml +++ /dev/null @@ -1,32 +0,0 @@ -# ⚠️ 검증 필요: 실제 운영 환경에 맞게 설정값 확인 후 사용하세요 -# - 포트 번호 -# - 메모리 제한 -# - 환경변수 파일 경로 -# - 컨테이너 이름 -# - 어드민 API 서버가 필요한지 확인 - -version: '3.8' - -services: - api: - image: ${FULL_IMAGE_NAME} - container_name: bottle-note-admin-api-production - ports: - - "40002:40002" - env_file: - - .bottlenote.admin.production.ENV - environment: - TZ: Asia/Seoul - SPRING_PROFILES_ACTIVE: prod - JAVA_OPTS: "-Xms512m -Xmx2g -XX:MaxMetaspaceSize=256m" - SERVER_NAME: oracle-node-prod-admin - GIT_BRANCH: ${GIT_BRANCH} - GIT_COMMIT: ${GIT_COMMIT} - BUILD_TIME: ${BUILD_TIME} - volumes: - - /var/log/bottle-note:/app/logs - deploy: - resources: - limits: - memory: 2g - restart: unless-stopped diff --git a/compose/product-api.dev.yml b/compose/product-api.dev.yml deleted file mode 100644 index 952a3c868..000000000 --- a/compose/product-api.dev.yml +++ /dev/null @@ -1,25 +0,0 @@ -version: '3.8' - -services: - api: - image: ${FULL_IMAGE_NAME} - container_name: bottle-note-api-development - ports: - - "30001:30001" - env_file: - - .bottlenote.development.ENV - environment: - TZ: Asia/Seoul - SPRING_PROFILES_ACTIVE: dev - JAVA_OPTS: "-Xms256m -Xmx512m -XX:MaxMetaspaceSize=128m" - SERVER_NAME: oracle-node-1 - GIT_BRANCH: ${GIT_BRANCH} - GIT_COMMIT: ${GIT_COMMIT} - BUILD_TIME: ${BUILD_TIME} - volumes: - - /var/log/bottle-note:/app/logs - deploy: - resources: - limits: - memory: 768m - restart: unless-stopped diff --git a/compose/product-api.prod.yml b/compose/product-api.prod.yml deleted file mode 100644 index 69f5f6c08..000000000 --- a/compose/product-api.prod.yml +++ /dev/null @@ -1,25 +0,0 @@ -version: '3.8' - -services: - api: - image: ${FULL_IMAGE_NAME} - container_name: bottle-note-api-production - ports: - - "30301:30301" - env_file: - - .bottlenote.production.ENV - environment: - TZ: Asia/Seoul - SPRING_PROFILES_ACTIVE: prod - JAVA_OPTS: "-Xms512m -Xmx2g -XX:MaxMetaspaceSize=256m" - SERVER_NAME: ${SERVER_NAME} - GIT_BRANCH: ${GIT_BRANCH} - GIT_COMMIT: ${GIT_COMMIT} - BUILD_TIME: ${BUILD_TIME} - volumes: - - /var/log/bottle-note:/app/logs - deploy: - resources: - limits: - memory: 3g - restart: always diff --git "a/http/admin/01_\354\235\270\354\246\235/\354\235\270\354\246\235.http" "b/http/admin/01_\354\235\270\354\246\235/\354\235\270\354\246\235.http" deleted file mode 100644 index 6164b8992..000000000 --- "a/http/admin/01_\354\235\270\354\246\235/\354\235\270\354\246\235.http" +++ /dev/null @@ -1,42 +0,0 @@ -### 로그인 -POST {{host}}/auth/login -Content-Type: application/json - -{ - "email": "{{email}}", - "password": "{{password}}" -} - -> {% - client.global.set("accessToken", response.body.data.accessToken); - client.global.set("refreshToken", response.body.data.refreshToken); -%} - -### 토큰 갱신 -POST {{host}}/auth/refresh -Content-Type: application/json - -{ - "refreshToken": "{{refreshToken}}" -} - -> {% - client.global.set("accessToken", response.body.data.accessToken); - client.global.set("refreshToken", response.body.data.refreshToken); -%} - -### 회원가입 -POST {{host}}/auth/signup -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "email": "new@bottlenote.com", - "password": "password123", - "name": "새 어드민", - "roles": ["PARTNER", "COMMUNITY_MANAGER"] -} - -### 탈퇴 -DELETE {{host}}/auth/withdraw -Authorization: Bearer {{accessToken}} diff --git "a/http/admin/02_\352\263\240\352\260\235\354\247\200\354\233\220/\353\254\270\354\235\230\352\264\200\353\246\254.http" "b/http/admin/02_\352\263\240\352\260\235\354\247\200\354\233\220/\353\254\270\354\235\230\352\264\200\353\246\254.http" deleted file mode 100644 index 0af07adca..000000000 --- "a/http/admin/02_\352\263\240\352\260\235\354\247\200\354\233\220/\353\254\270\354\235\230\352\264\200\353\246\254.http" +++ /dev/null @@ -1,18 +0,0 @@ -### 문의 목록 조회 -GET {{host}}/helps?cursor=0&pageSize=10 -Authorization: Bearer {{accessToken}} - -### 문의 상세 조회 -@helpId = 1 -GET {{host}}/helps/{{helpId}} -Authorization: Bearer {{accessToken}} - -### 문의 답변 등록 -@helpId = 1 -POST {{host}}/helps/{{helpId}}/answer -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "content": "문의 주신 내용에 대해 답변 드립니다." -} diff --git "a/http/admin/03_\354\225\214\354\275\224\354\230\254\352\264\200\353\246\254/\354\234\204\354\212\244\355\202\244.http" "b/http/admin/03_\354\225\214\354\275\224\354\230\254\352\264\200\353\246\254/\354\234\204\354\212\244\355\202\244.http" deleted file mode 100644 index 1df6af8ca..000000000 --- "a/http/admin/03_\354\225\214\354\275\224\354\230\254\352\264\200\353\246\254/\354\234\204\354\212\244\355\202\244.http" +++ /dev/null @@ -1,58 +0,0 @@ -### 위스키 목록 검색 -GET {{host}}/alcohols?keyword=&cursor=0&pageSize=10 -Authorization: Bearer {{accessToken}} - -### 위스키 상세 조회 -@alcoholId = 1 -GET {{host}}/alcohols/{{alcoholId}} -Authorization: Bearer {{accessToken}} - -### 위스키 생성 -POST {{host}}/alcohols -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "korName": "글렌피딕 12년", - "engName": "Glenfiddich 12 Year", - "abv": "40%", - "type": "WHISKY", - "korCategory": "싱글 몰트", - "engCategory": "Single Malt", - "categoryGroup": "SINGLE_MALT", - "regionId": 1, - "distilleryId": 1, - "age": "12", - "cask": "American Oak", - "imageUrl": "https://example.com/glenfiddich12.jpg", - "description": "스코틀랜드의 대표적인 싱글몰트 위스키", - "volume": "700ml", - "tastingTagIds": [1, 2, 3] -} - -### 위스키 수정 -PUT {{host}}/alcohols/{{alcoholId}} -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "korName": "글렌피딕 18년", - "engName": "Glenfiddich 18 Year", - "abv": "43%", - "type": "WHISKY", - "korCategory": "싱글 몰트", - "engCategory": "Single Malt", - "categoryGroup": "SINGLE_MALT", - "regionId": 1, - "distilleryId": 1, - "age": "18", - "cask": "Sherry Oak", - "imageUrl": "https://example.com/glenfiddich18.jpg", - "description": "18년 숙성 싱글몰트 위스키", - "volume": "700ml", - "tastingTagIds": [1, 2] -} - -### 위스키 삭제 -DELETE {{host}}/alcohols/{{alcoholId}} -Authorization: Bearer {{accessToken}} diff --git "a/http/admin/03_\354\225\214\354\275\224\354\230\254\352\264\200\353\246\254/\354\240\234\354\241\260\354\202\254_\354\247\200\354\227\255.http" "b/http/admin/03_\354\225\214\354\275\224\354\230\254\352\264\200\353\246\254/\354\240\234\354\241\260\354\202\254_\354\247\200\354\227\255.http" deleted file mode 100644 index c97424722..000000000 --- "a/http/admin/03_\354\225\214\354\275\224\354\230\254\352\264\200\353\246\254/\354\240\234\354\241\260\354\202\254_\354\247\200\354\227\255.http" +++ /dev/null @@ -1,7 +0,0 @@ -### 제조사 목록 조회 -GET {{host}}/distilleries?keyword=&cursor=0&pageSize=10 -Authorization: Bearer {{accessToken}} - -### 지역 목록 조회 -GET {{host}}/regions?keyword=&cursor=0&pageSize=10 -Authorization: Bearer {{accessToken}} diff --git "a/http/admin/03_\354\225\214\354\275\224\354\230\254\352\264\200\353\246\254/\354\271\264\355\205\214\352\263\240\353\246\254_\353\240\210\355\215\274\353\237\260\354\212\244.http" "b/http/admin/03_\354\225\214\354\275\224\354\230\254\352\264\200\353\246\254/\354\271\264\355\205\214\352\263\240\353\246\254_\353\240\210\355\215\274\353\237\260\354\212\244.http" deleted file mode 100644 index 13cc5f30d..000000000 --- "a/http/admin/03_\354\225\214\354\275\224\354\230\254\352\264\200\353\246\254/\354\271\264\355\205\214\352\263\240\353\246\254_\353\240\210\355\215\274\353\237\260\354\212\244.http" +++ /dev/null @@ -1,35 +0,0 @@ -### 로그인 (accessToken 발급 — 카테고리 레퍼런스 조회 전에 먼저 실행) -POST {{host}}/auth/login -Content-Type: application/json - -{ - "email": "{{email}}", - "password": "{{password}}" -} - -> {% - client.global.set("accessToken", response.body.data.accessToken); - client.global.set("refreshToken", response.body.data.refreshToken); -%} - -### 카테고리 레퍼런스 조회 (categoryGroup별 grouped Map) -# 응답은 AlcoholCategoryGroup enum(SINGLE_MALT, BLEND, BLENDED_MALT, BOURBON, RYE, OTHER)을 -# key로 갖고, 각 그룹에 속한 {korCategory, engCategory} 리스트를 value로 갖는다. -# 데이터가 없는 그룹도 빈 배열([])로 응답에 포함된다. -# -# 위스키 등록/수정(POST/PUT /alcohols) 시 사용해야 하는 categoryGroup 값을 -# 이 응답에서 단일 소스로 참조한다. -# -# 예시 응답: -# { -# "data": { -# "SINGLE_MALT": [{ "korCategory": "싱글 몰트", "engCategory": "Single Malt" }], -# "BLEND": [{ "korCategory": "블렌디드", "engCategory": "Blend" }], -# "BLENDED_MALT":[{ "korCategory": "블렌디드 몰트", "engCategory": "Blended Malt" }], -# "BOURBON": [{ "korCategory": "버번", "engCategory": "Bourbon" }], -# "RYE": [], -# "OTHER": [{ "korCategory": "테네시", "engCategory": "Tennessee" }] -# } -# } -GET {{host}}/alcohols/categories/reference -Authorization: Bearer {{accessToken}} diff --git "a/http/admin/03_\354\225\214\354\275\224\354\230\254\352\264\200\353\246\254/\355\205\214\354\235\264\354\212\244\355\214\205\355\203\234\352\267\270.http" "b/http/admin/03_\354\225\214\354\275\224\354\230\254\352\264\200\353\246\254/\355\205\214\354\235\264\354\212\244\355\214\205\355\203\234\352\267\270.http" deleted file mode 100644 index ec71c9b51..000000000 --- "a/http/admin/03_\354\225\214\354\275\224\354\230\254\352\264\200\353\246\254/\355\205\214\354\235\264\354\212\244\355\214\205\355\203\234\352\267\270.http" +++ /dev/null @@ -1,55 +0,0 @@ -### 테이스팅 태그 목록 조회 -GET {{host}}/tasting-tags?keyword=&page=0&size=20&sortOrder=ASC -Authorization: Bearer {{accessToken}} - -### 테이스팅 태그 상세 조회 -GET {{host}}/tasting-tags/1 -Authorization: Bearer {{accessToken}} - -### 테이스팅 태그 생성 -POST {{host}}/tasting-tags -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "korName": "바닐라", - "engName": "Vanilla", - "icon": "base64EncodedIcon", - "description": "바닐라 향", - "parentId": null -} - -### 테이스팅 태그 수정 -PUT {{host}}/tasting-tags/1 -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "korName": "바닐라 수정", - "engName": "Vanilla Updated", - "icon": "base64EncodedIcon", - "description": "수정된 바닐라 향", - "parentId": null -} - -### 테이스팅 태그 삭제 -DELETE {{host}}/tasting-tags/1 -Authorization: Bearer {{accessToken}} - -### 테이스팅 태그에 위스키 벌크 연결 -POST {{host}}/tasting-tags/1/alcohols -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "alcoholIds": [1, 2, 3] -} - -### 테이스팅 태그에서 위스키 벌크 연결 해제 -DELETE {{host}}/tasting-tags/1/alcohols -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "alcoholIds": [1, 2] -} diff --git "a/http/admin/04_\353\260\260\353\204\210\352\264\200\353\246\254/\353\260\260\353\204\210.http" "b/http/admin/04_\353\260\260\353\204\210\352\264\200\353\246\254/\353\260\260\353\204\210.http" deleted file mode 100644 index d720e612f..000000000 --- "a/http/admin/04_\353\260\260\353\204\210\352\264\200\353\246\254/\353\260\260\353\204\210.http" +++ /dev/null @@ -1,77 +0,0 @@ -### 배너 목록 조회 -GET {{host}}/banners?cursor=0&pageSize=10 -Authorization: Bearer {{accessToken}} - -### 배너 상세 조회 -@bannerId = 1 -GET {{host}}/banners/{{bannerId}} -Authorization: Bearer {{accessToken}} - -### 배너 생성 -POST {{host}}/banners -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "name": "새 배너", - "nameFontColor": "#ffffff", - "descriptionA": "배너 설명A", - "descriptionB": "배너 설명B", - "descriptionFontColor": "#ffffff", - "imageUrl": "https://example.com/banner.jpg", - "textPosition": "RT", - "isExternalUrl": false, - "targetUrl": "https://www.instagram.com/bottle_note_official", - "bannerType": "CURATION", - "sortOrder": 0, - "startDate": "2026-02-11T00:00:00", - "endDate": "2026-02-12T00:00:00" -} - -### 배너 수정 -@bannerId = 1 -PUT {{host}}/banners/{{bannerId}} -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "name": "수정된 배너", - "nameFontColor": "#000000", - "descriptionA": "수정된 설명A", - "descriptionB": "수정된 설명B", - "descriptionFontColor": "#000000", - "imageUrl": "https://example.com/banner-updated.jpg", - "textPosition": "RT", - "isExternalUrl": false, - "targetUrl": "https://www.instagram.com/bottle_note_official", - "bannerType": "CURATION", - "sortOrder": 1, - "startDate": "2026-02-11T00:00:00", - "endDate": "2026-02-28T00:00:00", - "isActive": true -} - -### 배너 삭제 -@bannerId = 1 -DELETE {{host}}/banners/{{bannerId}} -Authorization: Bearer {{accessToken}} - -### 배너 활성화 상태 변경 -@bannerId = 1 -PATCH {{host}}/banners/{{bannerId}}/status -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "isActive": true -} - -### 배너 정렬 순서 변경 -@bannerId = 1 -PATCH {{host}}/banners/{{bannerId}}/sort-order -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "sortOrder": 0 -} diff --git "a/http/admin/05_\355\201\220\353\240\210\354\235\264\354\205\230\352\264\200\353\246\254/\354\212\244\355\216\231\352\270\260\353\260\230\355\201\220\353\240\210\354\235\264\354\205\230v2.http" "b/http/admin/05_\355\201\220\353\240\210\354\235\264\354\205\230\352\264\200\353\246\254/\354\212\244\355\216\231\352\270\260\353\260\230\355\201\220\353\240\210\354\235\264\354\205\230v2.http" deleted file mode 100644 index ac52e8ba2..000000000 --- "a/http/admin/05_\355\201\220\353\240\210\354\235\264\354\205\230\352\264\200\353\246\254/\354\212\244\355\216\231\352\270\260\353\260\230\355\201\220\353\240\210\354\235\264\354\205\230v2.http" +++ /dev/null @@ -1,268 +0,0 @@ -### 로그인 -# v1 인증 API로 토큰을 받은 뒤 v2 큐레이션 관리 API를 호출한다. -POST {{host}}/auth/login -Content-Type: application/json - -{ - "email": "{{email}}", - "password": "{{password}}" -} - -> {% - client.global.set("accessToken", response.body.data.accessToken); - client.global.set("refreshToken", response.body.data.refreshToken); -%} - -### 1. 큐레이션 스펙 목록 조회 -# @no-cookie-jar -GET {{adminApiHost}}/v2/curation-specs -Authorization: Bearer {{accessToken}} - -> {% - const specs = response.body.data || []; - const recommended = specs.find((spec) => spec.code === "RECOMMENDED_WHISKY"); - const pairing = specs.find((spec) => spec.code === "WHISKY_PAIRING"); - const tasting = specs.find((spec) => spec.code === "WHISKY_TASTING_EVENT"); - - if (recommended) client.global.set("recommendedWhiskySpecId", recommended.id); - if (pairing) client.global.set("whiskyPairingSpecId", pairing.id); - if (tasting) client.global.set("whiskyTastingEventSpecId", tasting.id); -%} - -### 2. 큐레이션 스펙 상세 조회 -# @no-cookie-jar -@specId = {{recommendedWhiskySpecId}} -GET {{adminApiHost}}/v2/curation-specs/{{specId}} -Authorization: Bearer {{accessToken}} - -### 3. spec 기반 큐레이션 목록 조회 -# @no-cookie-jar -GET {{adminApiHost}}/v2/curations?keyword=&isActive=true&page=0&size=20 -Authorization: Bearer {{accessToken}} - -> {% - const page = response.body.data || {}; - const content = page.content || page.items || []; - if (content.length > 0) { - client.global.set("curationV2Id", content[0].id); - } -%} - -### 4. spec 기반 큐레이션 상세 조회 -# @no-cookie-jar -@curationId = {{curationV2Id}} -GET {{adminApiHost}}/v2/curations/{{curationId}} -Authorization: Bearer {{accessToken}} - -### 5. 추천 위스키 큐레이션 생성 -# @no-cookie-jar -POST {{adminApiHost}}/v2/curations -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "specId": {{recommendedWhiskySpecId}}, - "name": "비 오는 날 위스키", - "description": "스모키 위스키 추천", - "imageUrls": [ - "https://cdn.example.com/curation/rainy-whisky-cover.jpg" - ], - "exposureStartDate": "2026-06-01", - "exposureEndDate": "2026-06-30", - "displayOrder": 1, - "isActive": true, - "payload": [ - { - "source": "BOTTLE_NOTE", - "alcohol": { - "alcoholId": 1, - "korName": "검증 위스키", - "selectedTags": ["셰리", "오크"] - }, - "comment": "비 오는 날 천천히 마시기 좋은 위스키" - }, - { - "source": "MANUAL", - "alcohol": { - "alcoholId": null, - "korName": "수동 입력 위스키", - "selectedTags": ["스모키"] - }, - "comment": "외부 데이터 없이 직접 노출하는 항목" - } - ] -} - -> {% - if (response.body.data && response.body.data.targetId) { - client.global.set("curationV2Id", response.body.data.targetId); - } -%} - -### 6. 추천 위스키 큐레이션 수정 -# @no-cookie-jar -PUT {{adminApiHost}}/v2/curations/{{curationV2Id}} -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "specId": {{recommendedWhiskySpecId}}, - "name": "수정된 비 오는 날 위스키", - "description": "request spec 검증을 통과하는 수정 요청", - "imageUrls": [ - "https://cdn.example.com/curation/rainy-whisky-cover.jpg", - "https://cdn.example.com/curation/rainy-whisky-second.jpg" - ], - "exposureStartDate": "2026-06-01", - "exposureEndDate": "2026-06-30", - "displayOrder": 2, - "isActive": true, - "payload": [ - { - "source": "BOTTLE_NOTE", - "alcohol": { - "alcoholId": 1, - "korName": "검증 위스키", - "selectedTags": ["셰리", "오크", "스모키"] - }, - "comment": "수정된 추천 코멘트" - } - ] -} - -### 7. 위스키 페어링 큐레이션 생성 -# @no-cookie-jar -POST {{adminApiHost}}/v2/curations -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "specId": {{whiskyPairingSpecId}}, - "name": "디저트 페어링 위스키", - "description": "위스키와 어울리는 디저트 페어링", - "imageUrls": [ - "https://cdn.example.com/curation/pairing-cover.jpg" - ], - "exposureStartDate": "2026-06-01", - "exposureEndDate": "2026-06-30", - "displayOrder": 3, - "isActive": true, - "payload": [ - { - "source": "BOTTLE_NOTE", - "alcohol": { - "alcoholId": 1, - "korName": "글렌드로낙 오리지널 12년", - "selectedTags": ["셰리", "건포도", "오크"] - }, - "comment": "셰리 캐스크 입문용으로 추천", - "pairings": [ - { - "itemName": "부드러운 티라미수 초콜릿", - "itemImageUrl": "https://images.example.com/pairing/tiramisu.jpg", - "pairingNote": "진한 초콜릿 향이 위스키의 단맛과 이어진다." - } - ] - } - ] -} - -### 8. 위스키 시음회 큐레이션 생성 -# @no-cookie-jar -POST {{adminApiHost}}/v2/curations -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "specId": {{whiskyTastingEventSpecId}}, - "name": "6월 싱글몰트 시음회", - "description": "소규모 위스키 테이스팅 이벤트", - "imageUrls": [ - "https://cdn.example.com/curation/tasting-event-cover.jpg" - ], - "exposureStartDate": "2026-06-01", - "exposureEndDate": "2026-06-30", - "displayOrder": 4, - "isActive": true, - "payload": { - "eventDate": "2026-06-15", - "eventTime": "19:30", - "barAddress": "서울 강남구 테헤란로 123", - "detailAddress": "2층 도시남 바", - "isRecruiting": true, - "entryFee": 0, - "capacity": 12, - "applicationLink": "https://forms.example.com/tasting", - "guideText": "시작 10분 전 입장해 주세요.", - "alcohols": [ - { - "source": "BOTTLE_NOTE", - "alcohol": { - "alcoholId": 1, - "korName": "검증 위스키", - "selectedTags": ["셰리", "오크"] - }, - "comment": "시음회 대표 위스키" - } - ] - } -} - -### 9. 검증 실패 예시 - request spec required 누락 -# @no-cookie-jar -POST {{adminApiHost}}/v2/curations -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "specId": {{recommendedWhiskySpecId}}, - "name": "필수 필드 누락 테스트", - "description": "payload.source 누락으로 400 기대", - "imageUrls": [ - "https://cdn.example.com/curation/invalid-cover.jpg" - ], - "exposureStartDate": "2026-06-01", - "exposureEndDate": "2026-06-30", - "displayOrder": 99, - "isActive": true, - "payload": [ - { - "alcohol": { - "alcoholId": 1, - "korName": "검증 위스키", - "selectedTags": ["오크"] - }, - "comment": "source가 없어서 실패해야 한다." - } - ] -} - -### 10. 검증 실패 예시 - enum 값 불일치 -# @no-cookie-jar -POST {{adminApiHost}}/v2/curations -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "specId": {{recommendedWhiskySpecId}}, - "name": "enum 실패 테스트", - "description": "source enum 불일치로 400 기대", - "imageUrls": [ - "https://cdn.example.com/curation/invalid-cover.jpg" - ], - "exposureStartDate": "2026-06-01", - "exposureEndDate": "2026-06-30", - "displayOrder": 100, - "isActive": true, - "payload": [ - { - "source": "UNKNOWN_SOURCE", - "alcohol": { - "alcoholId": 1, - "korName": "검증 위스키", - "selectedTags": ["오크"] - }, - "comment": "source enum이 아니어서 실패해야 한다." - } - ] -} diff --git "a/http/admin/99_\352\263\265\355\206\265/\355\214\214\354\235\274\354\227\205\353\241\234\353\223\234.http" "b/http/admin/99_\352\263\265\355\206\265/\355\214\214\354\235\274\354\227\205\353\241\234\353\223\234.http" deleted file mode 100644 index 8f2d51f3e..000000000 --- "a/http/admin/99_\352\263\265\355\206\265/\355\214\214\354\235\274\354\227\205\353\241\234\353\223\234.http" +++ /dev/null @@ -1,3 +0,0 @@ -### S3 PreSigned URL 발급 -GET {{host}}/s3/presign-url?rootPath=admin&uploadSize=1 -Authorization: Bearer {{accessToken}} diff --git a/http/admin/http-client.env.json b/http/admin/http-client.env.json deleted file mode 100644 index f2936f938..000000000 --- a/http/admin/http-client.env.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "local": { - "host": "http://localhost:8080/admin/api/v1", - "adminApiHost": "http://localhost:8080/admin/api", - "email": "admin@bottlenote.com", - "password": "password123", - "accessToken": "", - "refreshToken": "" - }, - "dev": { - "host": "https://admin-api.development.bottle-note.com/admin/api/v1", - "adminApiHost": "https://admin-api.development.bottle-note.com/admin/api", - "email": "bottlenote.official@email.com", - "password": "password1234", - "accessToken": "", - "refreshToken": "" - } -} diff --git "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\352\260\200\354\236\205_\353\241\234\352\267\270\354\235\270/\354\206\214\354\205\234\355\232\214\354\233\220\352\260\200\354\236\205.http" "b/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\352\260\200\354\236\205_\353\241\234\352\267\270\354\235\270/\354\206\214\354\205\234\355\232\214\354\233\220\352\260\200\354\236\205.http" deleted file mode 100644 index d26aabc18..000000000 --- "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\352\260\200\354\236\205_\353\241\234\352\267\270\354\235\270/\354\206\214\354\205\234\355\232\214\354\233\220\352\260\200\354\236\205.http" +++ /dev/null @@ -1,10 +0,0 @@ -### 토큰 발급 (일반) -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "dev.bottle-note@gmail.com", - "socialType": "KAKAO", - "gender": "MALE", - "age": 25 -} diff --git "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\352\260\200\354\236\205_\353\241\234\352\267\270\354\235\270/\354\235\270\354\246\235v2.http" "b/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\352\260\200\354\236\205_\353\241\234\352\267\270\354\235\270/\354\235\270\354\246\235v2.http" deleted file mode 100644 index 28ac2090d..000000000 --- "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\352\260\200\354\236\205_\353\241\234\352\267\270\354\235\270/\354\235\270\354\246\235v2.http" +++ /dev/null @@ -1,23 +0,0 @@ -### 관리자 권한 확인 -GET {{host}}/api/v2/auth/admin/permissions -Authorization: Bearer {{accessToken}} - -### Apple 로그인 - Nonce 발급 -GET {{host}}/api/v2/auth/apple/nonce - -### Apple 로그인 -POST {{host}}/api/v2/auth/apple -Content-Type: application/json - -{ - "idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ik5qRXhOMFV3TlRVd05qYzRNVGxFUXpGRFFUVXlNekl4T0RBM01FVXdOREl5UVRBNVJqRXpSRGszT0EifQ...", - "nonce": "generated-nonce-value" -} - -### Kakao 로그인 -POST {{host}}/api/v2/auth/kakao -Content-Type: application/json - -{ - "accessToken": "kakao-access-token-here" -} diff --git "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\352\260\200\354\236\205_\353\241\234\352\267\270\354\235\270/\354\235\274\353\260\230\355\206\240\355\201\260\353\260\234\352\270\211.http" "b/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\352\260\200\354\236\205_\353\241\234\352\267\270\354\235\270/\354\235\274\353\260\230\355\206\240\355\201\260\353\260\234\352\270\211.http" deleted file mode 100644 index d8649de6b..000000000 --- "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\352\260\200\354\236\205_\353\241\234\352\267\270\354\235\270/\354\235\274\353\260\230\355\206\240\355\201\260\353\260\234\352\270\211.http" +++ /dev/null @@ -1,24 +0,0 @@ -### 토큰 발급 (일반) -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "dev.bottle-note@gmail.com", - "socialType": "GOOGLE", - "gender": "MALE", - "age": 25 -} - -> {% - //토큰을 전역변수로 저장 - client.global.set("accessToken", response.body.data.accessToken); -%} - -### 토큰이 없을 경우 발생하는 예외 예제 -PUT http://localhost:15000/api/v1/picks -Content-Type: application/json - -{ - "alcoholId": 1, - "isPicked": "PICK" -} diff --git "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\352\263\204\354\240\225\352\264\200\353\246\254/\354\234\240\354\240\200\353\246\254\355\217\254\355\212\270.http" "b/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\352\263\204\354\240\225\352\264\200\353\246\254/\354\234\240\354\240\200\353\246\254\355\217\254\355\212\270.http" deleted file mode 100644 index f5f4036b5..000000000 --- "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\352\263\204\354\240\225\352\264\200\353\246\254/\354\234\240\354\240\200\353\246\254\355\217\254\355\212\270.http" +++ /dev/null @@ -1,39 +0,0 @@ -### 토큰 발급 -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "juye@gmail.com", - "socialType": "GOOGLE", - "gender": "", - "age": "" -} - -> {% - //토큰을 전역변수로 저장 - client.global.set("accessToken", response.body.data.accessToken); -%} - -### 자신을 신고 할 수 없다. -POST {{host}}/api/v1/reports/user -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "userId" : 2, - "reportUserId" : 2, - "type" : "OTHER", - "content" : "신고 내용 쏼라 쏼라 쏼라 " -} - -### 사용자 신고 -POST {{host}}/api/v1/reports/user -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "userId" : 2, - "reportUserId" : 3, - "type" : "OTHER", - "content" : "신고 내용 쏼라 쏼라 쏼라 " -} diff --git "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\352\263\204\354\240\225\352\264\200\353\246\254/\355\206\240\355\201\260\352\262\200\354\246\235.http" "b/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\352\263\204\354\240\225\352\264\200\353\246\254/\355\206\240\355\201\260\352\262\200\354\246\235.http" deleted file mode 100644 index 6c74963d7..000000000 --- "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\352\263\204\354\240\225\352\264\200\353\246\254/\355\206\240\355\201\260\352\262\200\354\246\235.http" +++ /dev/null @@ -1,33 +0,0 @@ -@token = Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJoeWVqajE5QG5hdmVyLmNvbSIsInJvbGVzIjoiUk9MRV9VU0VSIiwidXNlcklkIjoxLCJpYXQiOjE3MzEyMDg0NTMsImV4cCI6MTczMTIwODUxM30.sBW0m-3vdIA0wJJkshFjlC2tGNU_ZfjfNwCP1tieL-8v-8LuNcGGu7XPrSZLzf8odVrNAqWyFK_7CeMV8iIIsA -@broken_token = Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJoeWVqajE5QG5hdmVyLmNvbSIsInJvbGVzIjoiUk9MRV9VU0VSIiwidXNlcklkIjoxLCJpYXQiOjE3MzEyMDg0NTMsImV4cCI6MTczMTIwODUxM30.sBW0m-3vdIA0wJJkshFjlC2tGNU_ZfjfNwCP1tieL-8 -@empty_token = Bearer - - -### 유저 아이디가 필수인 경우 -GET {{host}}/api/v1/my-page/8/my-bottle?keyword=®ionId=&tabType=ALL&sortType=LATEST&sortOrder=DESC&cursor=0& - pageSize=50 -Content-Type: application/json -Authorization: - - -### 토큰이 만료된 경우. -GET {{host}}/api/v1/my-page/8/my-bottle?keyword=®ionId=&tabType=ALL&sortType=LATEST&sortOrder=DESC&cursor=0& - pageSize=50 -Content-Type: application/json -Authorization: {{token}} - -### 토큰이 고장난 경우. -GET {{host}}/api/v1/my-page/8/my-bottle?keyword=®ionId=&tabType=ALL&sortType=LATEST&sortOrder=DESC&cursor=0& - pageSize=50 -Content-Type: application/json -Authorization: {{broken_token}} - -### 필수는 아니지만 토큰이 없는 경우 -GET {{host}}/api/v1/popular/week -Content-Type: application/json -Authorization: {{empty_token}} - -### 필수가 아니지만 토큰이 만료된 경우 -GET {{host}}/api/v1/reviews/1 -Content-Type: application/json -Authorization: {{token}} diff --git "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\352\263\204\354\240\225\352\264\200\353\246\254/\355\232\214\354\233\220\355\203\210\355\207\264.http" "b/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\352\263\204\354\240\225\352\264\200\353\246\254/\355\232\214\354\233\220\355\203\210\355\207\264.http" deleted file mode 100644 index 75963d7d4..000000000 --- "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\352\263\204\354\240\225\352\264\200\353\246\254/\355\232\214\354\233\220\355\203\210\355\207\264.http" +++ /dev/null @@ -1,3 +0,0 @@ -### 회원 탈퇴 -DELETE {{host}}/api/v1/users -Authorization: Bearer {{accessToken}} diff --git "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\353\202\264\354\240\225\353\263\264/\353\247\210\354\235\264\353\263\264\355\213\200.http" "b/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\353\202\264\354\240\225\353\263\264/\353\247\210\354\235\264\353\263\264\355\213\200.http" deleted file mode 100644 index c3a8f7d32..000000000 --- "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\353\202\264\354\240\225\353\263\264/\353\247\210\354\235\264\353\263\264\355\213\200.http" +++ /dev/null @@ -1,75 +0,0 @@ -### 토큰 발급 -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "juye@gmail.com", - "socialType": "GOOGLE", - "gender": "", - "age": "" -} - -> {% - client.global.set("accessToken", response.body.data.accessToken); -%} - - -### 로그인 이용자의 마이 보틀 페이지를 조회한다. (전체 타입, 최신순) -GET {{host}}/api/v1/my-page/2/my-bottle?keyword=®ionId=&tabType=ALL&sortType=LATEST&sortOrder=DESC&cursor=0& - pageSize=50 -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -### 로그인 이용자의 마이 보틀 페이지를 조회한다. (전체 타입, 평점순) -GET {{host}}/api/v1/my-page/8/my-bottle?keyword=®ionId=&tabType=ALL&sortType=RATING&sortOrder=DESC&cursor=0& - pageSize=50 -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -### 로그인 이용자의 마이 보틀 페이지를 조회한다. (전체 타입, 리뷰순) -GET {{host}}/api/v1/my-page/8/my-bottle?keyword=®ionId=&tabType=ALL&sortType=REVIEW&sortOrder=DESC&cursor=0& - pageSize=50 -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -### 로그인 이용자의 마이 보틀 페이지를 타입별로 조회한다. (리뷰) -GET {{host}}/api/v1/my-page/8/my-bottle?keyword=®ionId=&tabType=REVIEW&sortType=LATEST&sortOrder=DESC&cursor=0& - pageSize=50 -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -### 로그인 이용자의 마이 보틀 페이지를 타입별로 조회한다. (찜하기) -GET {{host}}/api/v1/my-page/8/my-bottle?keyword=®ionId=&tabType=PICK&sortType=LATEST&sortOrder=DESC&cursor=0& - pageSize=50 -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -### 로그인 이용자의 마이 보틀 페이지를 타입별로 조회한다. (평점) -GET {{host}}/api/v1/my-page/8/my-bottle?keyword=®ionId=&tabType=RATING&sortType=LATEST&sortOrder=DESC&cursor=0& - pageSize=50 -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -### 로그인 이용자의 마이 보틀 페이지를 키워드 'whisky'로 조회한다.(조회결과 없을것임) -GET {{host}}/api/v1/my-page/8/my-bottle?keyword=아란®ionId=&tabType=ALL&sortType=LATEST&sortOrder=DESC&cursor=0& - pageSize=50 -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -### 로그인 이용자의 마이 보틀 페이지를 지역으로 조회한다. -GET {{host}}/api/v1/my-page/8/my-bottle?keyword=®ionId=1&tabType=ALL&sortType=LATEST&sortOrder=DESC&cursor=0& - pageSize=50 -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -### 로그인 이용자의 마이 보틀 페이지를 오름차순으로 조회한다. -GET {{host}}/api/v1/my-page/8/my-bottle?keyword=®ionId=&tabType=ALL&sortType=LATEST&sortOrder=ASC&cursor=0& - pageSize=50 -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -### 로그인 이용자의 마이 보틀 페이지를 지역 12와 키워드 '라이'로 조회한다. -GET {{host}}/api/v1/my-page/8/my-bottle?keyword=라이®ionId=12&tabType=ALL&sortType=LATEST&sortOrder=DESC&cursor=0& - pageSize=50 -Content-Type: application/json -Authorization: Bearer {{accessToken}} diff --git "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\353\202\264\354\240\225\353\263\264/\353\247\210\354\235\264\353\263\264\355\213\200\354\204\270\353\266\200\354\241\260\355\232\214.http" "b/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\353\202\264\354\240\225\353\263\264/\353\247\210\354\235\264\353\263\264\355\213\200\354\204\270\353\266\200\354\241\260\355\232\214.http" deleted file mode 100644 index 1246302b6..000000000 --- "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\353\202\264\354\240\225\353\263\264/\353\247\210\354\235\264\353\263\264\355\213\200\354\204\270\353\266\200\354\241\260\355\232\214.http" +++ /dev/null @@ -1,29 +0,0 @@ -### 마이보틀 - 리뷰 목록 조회 -@userId = 8 -GET {{host}}/api/v1/my-page/{{userId}}/my-bottle/reviews?keyword=®ionId=&sortType=LATEST&sortOrder=DESC&cursor=0&pageSize=50 -Authorization: Bearer {{accessToken}} - -### 마이보틀 - 별점 목록 조회 -@userId = 8 -GET {{host}}/api/v1/my-page/{{userId}}/my-bottle/ratings?keyword=®ionId=&sortType=LATEST&sortOrder=DESC&cursor=0&pageSize=50 -Authorization: Bearer {{accessToken}} - -### 마이보틀 - 찜 목록 조회 -@userId = 8 -GET {{host}}/api/v1/my-page/{{userId}}/my-bottle/picks?keyword=®ionId=&sortType=LATEST&sortOrder=DESC&cursor=0&pageSize=50 -Authorization: Bearer {{accessToken}} - -### 마이보틀 - 리뷰 목록 (키워드 검색) -@userId = 8 -GET {{host}}/api/v1/my-page/{{userId}}/my-bottle/reviews?keyword=아란®ionId=&sortType=LATEST&sortOrder=DESC&cursor=0&pageSize=50 -Authorization: Bearer {{accessToken}} - -### 마이보틀 - 별점 목록 (지역 필터) -@userId = 8 -GET {{host}}/api/v1/my-page/{{userId}}/my-bottle/ratings?keyword=®ionId=1&sortType=LATEST&sortOrder=DESC&cursor=0&pageSize=50 -Authorization: Bearer {{accessToken}} - -### 마이보틀 - 찜 목록 (평점순) -@userId = 8 -GET {{host}}/api/v1/my-page/{{userId}}/my-bottle/picks?keyword=®ionId=&sortType=RATING&sortOrder=DESC&cursor=0&pageSize=50 -Authorization: Bearer {{accessToken}} diff --git "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\353\202\264\354\240\225\353\263\264/\353\247\210\354\235\264\355\216\230\354\235\264\354\247\200.http" "b/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\353\202\264\354\240\225\353\263\264/\353\247\210\354\235\264\355\216\230\354\235\264\354\247\200.http" deleted file mode 100644 index 954cb8a75..000000000 --- "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\353\202\264\354\240\225\353\263\264/\353\247\210\354\235\264\355\216\230\354\235\264\354\247\200.http" +++ /dev/null @@ -1,33 +0,0 @@ -### 토큰 발급 -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "juye@gmail.com", - "socialType": "GOOGLE", - "gender": "", - "age": "" -} - -> {% - //토큰을 전역변수로 저장 - client.global.set("accessToken", response.body.data.accessToken); -%} - -### 게스트 사용자가 마이페이지를 조회한다. -GET {{host}}/api/v1/my-page/1 - -### 로그인 이용자의 마이 페이지를 조회한다. -GET {{host}}/api/v1/my-page/8 -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -### 타인의 마이페이지를 조회한다. -GET {{host}}/api/v1/my-page/1 -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -### 존재하지 않는 사용자의 마이페이지를 조회한다. -GET {{host}}/api/v1/my-page/99999 -Content-Type: application/json -Authorization: Bearer {{accessToken}} diff --git "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\353\202\264\354\240\225\353\263\264/\355\224\204\353\241\234\355\225\204\354\210\230\354\240\225.http" "b/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\353\202\264\354\240\225\353\263\264/\355\224\204\353\241\234\355\225\204\354\210\230\354\240\225.http" deleted file mode 100644 index 3385011a8..000000000 --- "a/http/product/01_\355\232\214\354\233\220\352\264\200\353\246\254/\353\202\264\354\240\225\353\263\264/\355\224\204\353\241\234\355\225\204\354\210\230\354\240\225.http" +++ /dev/null @@ -1,21 +0,0 @@ -### 현재 사용자 정보 조회 -GET {{host}}/api/v1/users/current -Authorization: Bearer {{accessToken}} - -### 닉네임 변경 -PATCH {{host}}/api/v1/users/nickname -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "nickName": "juye123" -} - -### 프로필 이미지 변경 -PATCH {{host}}/api/v1/users/profile-image -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "viewUrl": "http://example.com/new-profile-image2.jpg" -} diff --git "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\353\202\230\353\247\214\354\235\230\354\234\204\354\212\244\355\202\244/\354\241\260\355\232\214\352\270\260\353\241\235.http" "b/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\353\202\230\353\247\214\354\235\230\354\234\204\354\212\244\355\202\244/\354\241\260\355\232\214\352\270\260\353\241\235.http" deleted file mode 100644 index 22ba09b51..000000000 --- "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\353\202\230\353\247\214\354\235\230\354\234\204\354\212\244\355\202\244/\354\241\260\355\232\214\352\270\260\353\241\235.http" +++ /dev/null @@ -1,22 +0,0 @@ -### 토큰 발급 -# @no-cookie-jar -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "rlagusrl928@gmail.com", - "socialType": "GOOGLE", - "gender": "", - "age": "" -} - -> {% - //토큰을 전역변수로 저장 - client.global.set("accessToken", response.body.data.accessToken); -%} - -### 위스키 조회 기록 조회 -# @no-cookie-jar -GET {{host}}/api/v1/history/view/alcohols -Content-Type: application/json -Authorization: Bearer {{accessToken}} diff --git "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\353\202\230\353\247\214\354\235\230\354\234\204\354\212\244\355\202\244/\354\260\234\355\225\230\352\270\260.http" "b/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\353\202\230\353\247\214\354\235\230\354\234\204\354\212\244\355\202\244/\354\260\234\355\225\230\352\270\260.http" deleted file mode 100644 index 36ac663b6..000000000 --- "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\353\202\230\353\247\214\354\235\230\354\234\204\354\212\244\355\202\244/\354\260\234\355\225\230\352\270\260.http" +++ /dev/null @@ -1,44 +0,0 @@ -### 토큰 발급 -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "juye@gmail.com", - "socialType": "GOOGLE", - "gender": "", - "age": "" -} -> {% - //토큰을 전역변수로 저장 - client.global.set("accessToken", response.body.data.accessToken); -%} - -### 위스키 찜하기 -PUT {{host}}/api/v1/picks -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "alcoholId": 1, - "isPicked": "PICK" -} - -### 위스키 찜하기 -PUT {{host}}/api/v1/picks -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "alcoholId": -1, - "isPicked": "" -} - -### 위스키 찜하기 (예외 2개 발생 샘플 케이스) -PUT {{host}}/api/v1/picks -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "alcoholId": -1, - "isPicked": "" -} diff --git "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\240\225\353\263\264/\353\221\230\353\237\254\353\263\264\352\270\260.http" "b/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\240\225\353\263\264/\353\221\230\353\237\254\353\263\264\352\270\260.http" deleted file mode 100644 index 80d2aa829..000000000 --- "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\240\225\353\263\264/\353\221\230\353\237\254\353\263\264\352\270\260.http" +++ /dev/null @@ -1,63 +0,0 @@ -### 토큰 발급 (일반) -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "dev.bottle-note@gmail.com", - "socialType": "GOOGLE", - "gender": "MALE", - "age": 25 -} - -> {% - //토큰을 전역변수로 저장 - client.global.set("accessToken", response.body.data.accessToken); -%} - -### 1. 위스키 둘러보기 - 다중 키워드 (AND) -GET {{host}}/api/v1/alcohols/explore/standard?cursor=0&size=6&keywords=탈라모어&keywords=사과 -Authorization: Bearer {{accessToken}} - -### 2. 위스키 둘러보기 - 기본 랜덤 탐색 -GET {{host}}/api/v1/alcohols/explore/standard?cursor=0&size=10 -Authorization: Bearer {{accessToken}} - -### 3. 위스키 둘러보기 - 카테고리 필터 -GET {{host}}/api/v1/alcohols/explore/standard?category=SINGLE_MALT&size=10 -Authorization: Bearer {{accessToken}} - -### 4. 위스키 둘러보기 - regionIds 복수 OR 필터 -GET {{host}}/api/v1/alcohols/explore/standard?regionIds=1®ionIds=2&size=10 -Authorization: Bearer {{accessToken}} - -### 5. 위스키 둘러보기 - distilleryIds 복수 OR 필터 -GET {{host}}/api/v1/alcohols/explore/standard?distilleryIds=10&distilleryIds=20&size=10 -Authorization: Bearer {{accessToken}} - -### 6. 위스키 둘러보기 - 큐레이션 필터 -GET {{host}}/api/v1/alcohols/explore/standard?curationId=1&size=10 -Authorization: Bearer {{accessToken}} - -### 7. 위스키 둘러보기 - 인기순 정렬(POPULAR + DESC) -GET {{host}}/api/v1/alcohols/explore/standard?sortType=POPULAR&sortOrder=DESC&size=10 -Authorization: Bearer {{accessToken}} - -### 8. 위스키 둘러보기 - 평점순 정렬(RATING + DESC) -GET {{host}}/api/v1/alcohols/explore/standard?sortType=RATING&sortOrder=DESC&size=10 -Authorization: Bearer {{accessToken}} - -### 9. 위스키 둘러보기 - 리뷰순 정렬(REVIEW + DESC) -GET {{host}}/api/v1/alcohols/explore/standard?sortType=REVIEW&sortOrder=DESC&size=10 -Authorization: Bearer {{accessToken}} - -### 10. 위스키 둘러보기 - 찜순 정렬(PICK + DESC) -GET {{host}}/api/v1/alcohols/explore/standard?sortType=PICK&sortOrder=DESC&size=10 -Authorization: Bearer {{accessToken}} - -### 11. 위스키 둘러보기 - 복합 필터 + 정렬 -GET {{host}}/api/v1/alcohols/explore/standard?keywords=글렌&category=SINGLE_MALT®ionIds=1®ionIds=2&sortType=POPULAR&sortOrder=DESC&size=10 -Authorization: Bearer {{accessToken}} - -### 12. 위스키 둘러보기 - 커서 페이지네이션 -GET {{host}}/api/v1/alcohols/explore/standard?cursor=10&size=10 -Authorization: Bearer {{accessToken}} diff --git "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\240\225\353\263\264/\354\203\201\354\204\270\353\263\264\352\270\260.http" "b/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\240\225\353\263\264/\354\203\201\354\204\270\353\263\264\352\270\260.http" deleted file mode 100644 index f2b6bcd3c..000000000 --- "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\240\225\353\263\264/\354\203\201\354\204\270\353\263\264\352\270\260.http" +++ /dev/null @@ -1,20 +0,0 @@ -### 토큰 발급 -# @no-cookie-jar -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "rlagusrl928@gmail.com", - "socialType": "GOOGLE" -} - -> {% - //토큰을 전역변수로 저장 - client.global.set("accessToken", response.body.data.accessToken); -%} - -### 위스키 조회 상세 조회 -# @no-cookie-jar -@alcoholId=209 -GET {{host}}/api/v1/alcohols/{{alcoholId}} -Authorization: Bearer {{accessToken}} diff --git "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\240\225\353\263\264/\354\271\264\355\205\214\352\263\240\353\246\254_\354\247\200\354\227\255.http" "b/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\240\225\353\263\264/\354\271\264\355\205\214\352\263\240\353\246\254_\354\247\200\354\227\255.http" deleted file mode 100644 index ccb742474..000000000 --- "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\240\225\353\263\264/\354\271\264\355\205\214\352\263\240\353\246\254_\354\247\200\354\227\255.http" +++ /dev/null @@ -1,8 +0,0 @@ -### 카테고리 조회 -GET {{host}}/api/v1/alcohols/categories -Content-Type: application/json - -### 지역 조회 -GET {{host}}/api/v1/regions -Content-Type: application/json -Authorization: Bearer {{accessToken}} diff --git "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\260\276\352\270\260/\354\235\270\352\270\260\354\234\204\354\212\244\355\202\244.http" "b/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\260\276\352\270\260/\354\235\270\352\270\260\354\234\204\354\212\244\355\202\244.http" deleted file mode 100644 index ccc14e233..000000000 --- "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\260\276\352\270\260/\354\235\270\352\270\260\354\234\204\354\212\244\355\202\244.http" +++ /dev/null @@ -1,31 +0,0 @@ -### 토큰 발급 -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "juye@gmail.com", - "socialType": "GOOGLE", - "gender": "", - "age": "" -} - -> {% - //토큰을 전역변수로 저장 - client.global.set("accessToken", response.body.data.accessToken); -%} - -### 주간 인기 위스키 조회 - -GET {{host}}/api/v1/popular/week -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -> {% - //토큰을 전역변수로 저장 - client.global.set("accessToken", response.body.data.accessToken); -%} - -### 봄에 어울리는 위스키 조회 -GET {{host}}/api/v1/popular/spring -Content-Type: application/json -Authorization: Bearer {{accessToken}} diff --git "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\260\276\352\270\260/\355\201\220\353\240\210\354\235\264\354\205\230v2.http" "b/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\260\276\352\270\260/\355\201\220\353\240\210\354\235\264\354\205\230v2.http" deleted file mode 100644 index 86f82a0bf..000000000 --- "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\260\276\352\270\260/\355\201\220\353\240\210\354\235\264\354\205\230v2.http" +++ /dev/null @@ -1,39 +0,0 @@ -### 토큰 발급 (일반) -# @no-cookie-jar -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "dev.bottle-note@gmail.com", - "socialType": "GOOGLE", - "gender": "MALE", - "age": 25 -} - -> {% - client.global.set("accessToken", response.body.data.accessToken); -%} - -### 1. spec 기반 큐레이션 v2 목록 조회 -# @no-cookie-jar -GET {{host}}/api/v2/curations -Authorization: Bearer {{accessToken}} - -> {% - const curations = response.body.data || []; - if (curations.length > 0) { - client.global.set("curationV2Id", curations[0].id); - } -%} - -### 2. spec 기반 큐레이션 v2 상세 조회 -# @no-cookie-jar -@curationId = {{curationV2Id}} -GET {{host}}/api/v2/curations/{{curationId}} -Authorization: Bearer {{accessToken}} - -### 3. spec 기반 큐레이션 v2 상세 조회 - 직접 ID 지정 -# @no-cookie-jar -@manualCurationId = 1 -GET {{host}}/api/v2/curations/{{manualCurationId}} -Authorization: Bearer {{accessToken}} diff --git "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\260\276\352\270\260/\355\201\220\353\240\210\354\235\264\354\205\230v2_\354\213\234\354\235\214\355\232\214_\355\224\274\353\223\234\352\262\200\354\246\235.http" "b/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\260\276\352\270\260/\355\201\220\353\240\210\354\235\264\354\205\230v2_\354\213\234\354\235\214\355\232\214_\355\224\274\353\223\234\352\262\200\354\246\235.http" deleted file mode 100644 index 4b337e028..000000000 --- "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\260\276\352\270\260/\355\201\220\353\240\210\354\235\264\354\205\230v2_\354\213\234\354\235\214\355\232\214_\355\224\274\353\223\234\352\262\200\354\246\235.http" +++ /dev/null @@ -1,21 +0,0 @@ -### 1. 앱 정보 조회 - 배포 commit 확인 -# @no-cookie-jar -GET {{host}}/api/v1/app-info - -### 2. spec 기반 큐레이션 v2 목록 조회 - 시음회 ID 확인 -# @no-cookie-jar -GET {{host}}/api/v2/curations - -### 3. 시음회 스펙 조회 - alcohols.x-feed 미존재 확인 -# @no-cookie-jar -@whiskyTastingEventSpecId = 3 -GET {{host}}/api/v2/curation-specs/{{whiskyTastingEventSpecId}} - -### 4. spec 기반 큐레이션 v2 피드 조회 - 시음회 payload.alcohols 미노출 확인 -# @no-cookie-jar -GET {{host}}/api/v2/curations/feed?cursor=0&size=10 - -### 5. 시음회 상세 조회 - 상세 payload.alcohols 유지 확인 -# @no-cookie-jar -@whiskyTastingEventCurationId = 4 -GET {{host}}/api/v2/curations/{{whiskyTastingEventCurationId}} diff --git "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\260\276\352\270\260/\355\201\220\353\240\210\354\235\264\354\205\230\352\262\200\354\203\211.http" "b/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\260\276\352\270\260/\355\201\220\353\240\210\354\235\264\354\205\230\352\262\200\354\203\211.http" deleted file mode 100644 index 8a06c5898..000000000 --- "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\260\276\352\270\260/\355\201\220\353\240\210\354\235\264\354\205\230\352\262\200\354\203\211.http" +++ /dev/null @@ -1,51 +0,0 @@ -### 토큰 발급 (일반) -# @no-cookie-jar -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "dev.bottle-note@gmail.com", - "socialType": "GOOGLE", - "gender": "MALE", - "age": 25 -} - -> {% - //토큰을 전역변수로 저장 - client.global.set("accessToken", response.body.data.accessToken); -%} - -### 1. 큐레이션 키워드 전체 조회 -# @no-cookie-jar -# 활성화된 모든 큐레이션 키워드 조회 -GET {{host}}/api/v1/curations -Authorization: Bearer {{accessToken}} - -### 2. 큐레이션 키워드 검색 (키워드로) -# @no-cookie-jar -# 큐레이션 이름에 특정 키워드가 포함된 큐레이션 검색 -GET {{host}}/api/v1/curations?keyword=추천 -Authorization: Bearer {{accessToken}} - -### 3. 큐레이션 키워드 검색 (알코올 ID로) -# @no-cookie-jar -# 특정 알코올이 포함된 큐레이션 검색 -GET {{host}}/api/v1/curations?alcoholId=4728 -Authorization: Bearer {{accessToken}} - -### 4. 큐레이션 키워드 검색 (커서 페이지네이션) -# @no-cookie-jar -GET {{host}}/api/v1/curations?cursor=0&pageSize=10 -Authorization: Bearer {{accessToken}} - -### 5. 특정 큐레이션에 포함된 알코올 조회 -# @no-cookie-jar -@curationId = 1 -GET {{host}}/api/v1/curations/{{curationId}}/alcohols -Authorization: Bearer {{accessToken}} - -### 6. 특정 큐레이션에 포함된 알코올 조회 (페이지네이션) -# @no-cookie-jar -@curationId = 1 -GET {{host}}/api/v1/curations/{{curationId}}/alcohols?cursor=0&pageSize=10 -Authorization: Bearer {{accessToken}} diff --git "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\260\276\352\270\260/\355\206\265\355\225\251\352\262\200\354\203\211.http" "b/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\260\276\352\270\260/\355\206\265\355\225\251\352\262\200\354\203\211.http" deleted file mode 100644 index 85293a609..000000000 --- "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\354\234\204\354\212\244\355\202\244\354\260\276\352\270\260/\355\206\265\355\225\251\352\262\200\354\203\211.http" +++ /dev/null @@ -1,89 +0,0 @@ -### 토큰 발급 (일반) -# @no-cookie-jar -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "dev.bottle-note@gmail.com", - "socialType": "GOOGLE", - "gender": "MALE", - "age": 25 -} - -> {% - //토큰을 전역변수로 저장 - client.global.set("accessToken", response.body.data.accessToken); -%} - -### 1. 기본 검색 (모든 알코올 조회) -# @no-cookie-jar -GET {{host}}/api/v1/alcohols/search -Authorization: Bearer {{accessToken}} - -### 2. 키워드 검색 (단일 단어) -# @no-cookie-jar -GET {{host}}/api/v1/alcohols/search?keyword=비 오는 날 추천 위스키 -Authorization: Bearer {{accessToken}} - -### 3. 키워드 검색 (여러 단어 - AND 검색) -# @no-cookie-jar -GET {{host}}/api/v1/alcohols/search?keyword=조니 워커 블랙 -Authorization: Bearer {{accessToken}} - -### 4. 큐레이션 ID로 검색 -# @no-cookie-jar -# 특정 큐레이션에 포함된 알코올만 필터링 -GET {{host}}/api/v1/alcohols/search?curationId=1 -Authorization: Bearer {{accessToken}} - -### 5. 큐레이션 ID + 키워드 검색 (curationId 우선 적용) -# @no-cookie-jar -GET {{host}}/api/v1/alcohols/search?curationId=1&keyword=맥캘란 -Authorization: Bearer {{accessToken}} - -### 6. 카테고리 필터 검색 -# @no-cookie-jar -GET {{host}}/api/v1/alcohols/search?category=SINGLE_MALT&pageSize=10 -Authorization: Bearer {{accessToken}} - -### 7. 카테고리 + 지역 필터 검색 -# @no-cookie-jar -GET {{host}}/api/v1/alcohols/search?category=SINGLE_MALT®ionId=1 -Authorization: Bearer {{accessToken}} - -### 8. 정렬 조건 포함 검색 (리뷰 많은 순) -# @no-cookie-jar -GET {{host}}/api/v1/alcohols/search?sortType=REVIEW&sortOrder=DESC&pageSize=10 -Authorization: Bearer {{accessToken}} - -### 9. 정렬 조건 포함 검색 (평점 높은 순) -# @no-cookie-jar -GET {{host}}/api/v1/alcohols/search?sortType=RATING&sortOrder=DESC&pageSize=10 -Authorization: Bearer {{accessToken}} - -### 10. 복합 검색 (키워드 + 카테고리 + 정렬) -# @no-cookie-jar -GET {{host}}/api/v1/alcohols/search?keyword=글렌&category=SINGLE_MALT&sortType=POPULAR&sortOrder=DESC&pageSize=10 -Authorization: Bearer {{accessToken}} - -### 11. 커서 페이지네이션 (다음 페이지) -# @no-cookie-jar -# cursor 값은 이전 응답의 meta.pageable.cursor 값 사용 -GET {{host}}/api/v1/alcohols/search?cursor=10&pageSize=10 -Authorization: Bearer {{accessToken}} - -### 위스키 상세 조회 -# @no-cookie-jar -@alcoholId = 4728 -GET {{host}}/api/v1/alcohols/{{alcoholId}} -Authorization: Bearer {{accessToken}} - -### 카테고리 조회 -# @no-cookie-jar -GET {{host}}/api/v1/alcohols/categories -Authorization: Bearer {{accessToken}} - -### 지역 조회 -# @no-cookie-jar -GET {{host}}/api/v1/alcohols/regions -Authorization: Bearer {{accessToken}} diff --git "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\355\205\214\354\235\264\354\212\244\355\214\205\355\203\234\352\267\270/\355\203\234\352\267\270\354\266\224\354\266\234.http" "b/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\355\205\214\354\235\264\354\212\244\355\214\205\355\203\234\352\267\270/\355\203\234\352\267\270\354\266\224\354\266\234.http" deleted file mode 100644 index 4911c58f9..000000000 --- "a/http/product/02_\354\234\204\354\212\244\355\202\244\355\203\220\354\203\211/\355\205\214\354\235\264\354\212\244\355\214\205\355\203\234\352\267\270/\355\203\234\352\267\270\354\266\224\354\266\234.http" +++ /dev/null @@ -1,30 +0,0 @@ -### 토큰 발급 -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "dev.bottle-note@gmail.com", - "socialType": "GOOGLE", - "gender": "MALE", - "age": 25 -} - -> {% - client.global.set("accessToken", response.body.data.accessToken); -%} - -### 테이스팅 태그 추출 - 기본 -GET {{host}}/api/v1/tasting-tags/extract?text=바닐라 향이 좋고 꿀 같은 단맛이 느껴져요 -Authorization: Bearer {{accessToken}} - -### 테이스팅 태그 추출 - 여러 태그 -GET {{host}}/api/v1/tasting-tags/extract?text=스모키 하면서 피트 향이 강하고 오크 통 숙성의 깊은 맛 -Authorization: Bearer {{accessToken}} - -### 테이스팅 태그 추출 - 매칭 없음 -GET {{host}}/api/v1/tasting-tags/extract?text=그냥 평범한 위스키입니다 -Authorization: Bearer {{accessToken}} - -### 테이스팅 태그 추출 - 포함 매칭 -GET {{host}}/api/v1/tasting-tags/extract?text=스모키향이 좋아요 -Authorization: Bearer {{accessToken}} diff --git "a/http/product/03_\353\246\254\353\267\260_\355\217\211\352\260\200/\353\214\223\352\270\200/\353\214\223\352\270\200\352\264\200\353\246\254.http" "b/http/product/03_\353\246\254\353\267\260_\355\217\211\352\260\200/\353\214\223\352\270\200/\353\214\223\352\270\200\352\264\200\353\246\254.http" deleted file mode 100644 index 4d513fded..000000000 --- "a/http/product/03_\353\246\254\353\267\260_\355\217\211\352\260\200/\353\214\223\352\270\200/\353\214\223\352\270\200\352\264\200\353\246\254.http" +++ /dev/null @@ -1,24 +0,0 @@ -### 리뷰 댓글 작성 -@reviewId = 5 -POST {{host}}/api/v1/review/reply/register/{{reviewId}} -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "content": "👍👍👍👍", - "parentReplyId": "4" -} - -### 리뷰 댓글 조회 -@reviewId = 1 -GET {{host}}/api/v1/review/reply/{{reviewId}} - -### 리뷰 대댓글 조회 -@replyId = 1 -GET {{host}}/api/v1/review/reply/{{reviewId}}/sub/{{replyId}} - -### 리뷰 댓글 삭제 -@reviewId = 5 -@replyId = 23 -DELETE {{host}}/api/v1/review/reply/{{reviewId}}/{{replyId}} -Authorization: Bearer {{accessToken}} diff --git "a/http/product/03_\353\246\254\353\267\260_\355\217\211\352\260\200/\353\246\254\353\267\260/\353\246\254\353\267\260\353\263\264\352\270\260.http" "b/http/product/03_\353\246\254\353\267\260_\355\217\211\352\260\200/\353\246\254\353\267\260/\353\246\254\353\267\260\353\263\264\352\270\260.http" deleted file mode 100644 index c5014315a..000000000 --- "a/http/product/03_\353\246\254\353\267\260_\355\217\211\352\260\200/\353\246\254\353\267\260/\353\246\254\353\267\260\353\263\264\352\270\260.http" +++ /dev/null @@ -1,20 +0,0 @@ -### 리뷰 조회 -# @no-cookie-jar -@alcoholId=209 -GET {{host}}/api/v1/reviews/{{alcoholId}} -Authorization: Bearer {{accessToken}} - -### 나의 리뷰 조회 -GET {{host}}/api/v1/reviews/me/{{alcoholId}} -Authorization: Bearer {{accessToken}} - -### 리뷰 상세 조회 -# @no-cookie-jar -@reviewId = 2 -GET {{host}}/api/v1/reviews/detail/{{reviewId}} -Authorization: Bearer {{accessToken}} - -### 리뷰 둘러보기 -# @no-cookie-jar -GET {{host}}/api/v1/reviews/explore/standard?page=0&size=10&keywords=시그넷 -Authorization: Bearer {{accessToken}} diff --git "a/http/product/03_\353\246\254\353\267\260_\355\217\211\352\260\200/\353\246\254\353\267\260/\353\246\254\353\267\260\354\236\221\354\204\261_\354\210\230\354\240\225_\354\202\255\354\240\234.http" "b/http/product/03_\353\246\254\353\267\260_\355\217\211\352\260\200/\353\246\254\353\267\260/\353\246\254\353\267\260\354\236\221\354\204\261_\354\210\230\354\240\225_\354\202\255\354\240\234.http" deleted file mode 100644 index 4d04150f6..000000000 --- "a/http/product/03_\353\246\254\353\267\260_\355\217\211\352\260\200/\353\246\254\353\267\260/\353\246\254\353\267\260\354\236\221\354\204\261_\354\210\230\354\240\225_\354\202\255\354\240\234.http" +++ /dev/null @@ -1,89 +0,0 @@ -### 리뷰 작성 -POST {{host}}/api/v1/reviews -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "alcoholId": 1, - "status": "PUBLIC", - "content": "정말 맛있는 위스키입니다. 부드러운 피트향과 달콤한 과일향이 잘 어우러져 있어요.", - "sizeType": "BOTTLE", - "price": 89000, - "locationInfo": { - "locationName": "위스키샵 강남점", - "zipCode": "06123", - "address": "서울특별시 강남구 테헤란로 123", - "detailAddress": "1층 101호", - "category": "LIQUOR_STORE", - "mapUrl": "https://map.kakao.com/link/123", - "latitude": "37.1234", - "longitude": "127.5678" - }, - "imageUrlList": [ - { - "order": 1, - "viewUrl": "https://example.com/image1.jpg" - }, - { - "order": 2, - "viewUrl": "https://example.com/image2.jpg" - } - ], - "tastingTagList": ["SMOKY", "SWEET", "FRUITY"], - "rating": 4.5 -} - -### 리뷰 수정 -@reviewId = 1 -PATCH {{host}}/api/v1/reviews/{{reviewId}} -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "content": "수정된 리뷰 내용입니다. 다시 맛보니 더 좋네요.", - "status": "PUBLIC", - "price": 95000, - "imageUrlList": [ - { - "order": 1, - "viewUrl": "https://example.com/updated-image1.jpg" - } - ], - "sizeType": "GLASS", - "tastingTagList": ["SMOKY", "SWEET"], - "locationInfo": { - "locationName": "위스키바 홍대점", - "zipCode": "04123", - "address": "서울특별시 마포구 홍익로 45", - "detailAddress": "2층", - "category": "BAR", - "mapUrl": "https://map.kakao.com/link/456", - "latitude": "37.5567", - "longitude": "126.9236" - } -} - -### 리뷰 공개 여부 변경 -@reviewId = 1 -PATCH {{host}}/api/v1/reviews/{{reviewId}}/display -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "status": "PRIVATE" -} - -### 리뷰 공개로 변경 -@reviewId = 1 -PATCH {{host}}/api/v1/reviews/{{reviewId}}/display -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "status": "PUBLIC" -} - -### 리뷰 삭제 -@reviewId = 1 -DELETE {{host}}/api/v1/reviews/{{reviewId}} -Authorization: Bearer {{accessToken}} diff --git "a/http/product/03_\353\246\254\353\267\260_\355\217\211\352\260\200/\353\246\254\353\267\260/\353\246\254\353\267\260\354\242\213\354\225\204\354\232\224.http" "b/http/product/03_\353\246\254\353\267\260_\355\217\211\352\260\200/\353\246\254\353\267\260/\353\246\254\353\267\260\354\242\213\354\225\204\354\232\224.http" deleted file mode 100644 index 2f0cfd570..000000000 --- "a/http/product/03_\353\246\254\353\267\260_\355\217\211\352\260\200/\353\246\254\353\267\260/\353\246\254\353\267\260\354\242\213\354\225\204\354\232\224.http" +++ /dev/null @@ -1,41 +0,0 @@ -### 토큰 발급 (일반) -# @no-cookie-jar -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "dev.bottle-note@gmail.com", - "socialType": "GOOGLE", - "gender": "MALE", - "age": 25 -} - -> {% - //토큰을 전역변수로 저장 - client.global.set("accessToken", response.body.data.accessToken); -%} - - -### 리뷰 좋아요 -# @no-cookie-jar -@reviewId = "9" -@status = "LIKE" -PUT {{host}}/api/v1/likes -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "reviewId": {{reviewId}}, - "status": {{status}} -} - -### 리뷰 싫어요 -@badReviewId = "-1" -PUT {{host}}/api/v1/likes -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "reviewId": {{badReviewId}}, - "status": {{status}} -} diff --git "a/http/product/03_\353\246\254\353\267\260_\355\217\211\352\260\200/\353\263\204\354\240\220/\353\263\204\354\240\220\352\264\200\353\246\254.http" "b/http/product/03_\353\246\254\353\267\260_\355\217\211\352\260\200/\353\263\204\354\240\220/\353\263\204\354\240\220\352\264\200\353\246\254.http" deleted file mode 100644 index 783e307a8..000000000 --- "a/http/product/03_\353\246\254\353\267\260_\355\217\211\352\260\200/\353\263\204\354\240\220/\353\263\204\354\240\220\352\264\200\353\246\254.http" +++ /dev/null @@ -1,33 +0,0 @@ -### 별점 등록 -POST {{host}}/api/v1/rating/register -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "alcoholId": 1, - "rating": 3.5 -} - -### 별점은 0.5점 단위로 등록 -POST {{host}}/api/v1/rating/register -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "alcoholId": 1, - "rating": 0.1 -} - -### 별점 목록 조회 -@keyword = -@category = SINGLE_MALT -@regionId = -@cursor = 0 -@pageSize = 10 -GET {{host}}/api/v1/rating?keyword={{keyword}}&category={{category}}®ionId={{regionId}}&sortType=RANDOM&sortOrder=DESC&cursor={{cursor}}&pageSize={{pageSize}} - -### 유저별 별점 조회 -@alcoholId = 209 -GET {{host}}/api/v1/rating/{{alcoholId}} -Content-Type: application/json -Authorization: Bearer {{accessToken}} diff --git "a/http/product/03_\353\246\254\353\267\260_\355\217\211\352\260\200/\354\213\240\352\263\240/\354\213\240\352\263\240\352\264\200\353\246\254.http" "b/http/product/03_\353\246\254\353\267\260_\355\217\211\352\260\200/\354\213\240\352\263\240/\354\213\240\352\263\240\352\264\200\353\246\254.http" deleted file mode 100644 index 69a4dca0c..000000000 --- "a/http/product/03_\353\246\254\353\267\260_\355\217\211\352\260\200/\354\213\240\352\263\240/\354\213\240\352\263\240\352\264\200\353\246\254.http" +++ /dev/null @@ -1,21 +0,0 @@ -### 리뷰 신고 -POST {{host}}/api/v1/reports/review -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "reviewId": 1, - "reportType": "INAPPROPRIATE_CONTENT", - "content": "부적절한 내용이 포함되어 있습니다." -} - -### 사용자 신고 -POST {{host}}/api/v1/reports/user -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "reportUserId": 10, - "reportType": "SPAM", - "content": "스팸 활동을 하는 사용자입니다." -} diff --git "a/http/product/04_\354\206\214\354\205\234/\354\260\250\353\213\250\352\264\200\353\246\254.http" "b/http/product/04_\354\206\214\354\205\234/\354\260\250\353\213\250\352\264\200\353\246\254.http" deleted file mode 100644 index 3c34451b1..000000000 --- "a/http/product/04_\354\206\214\354\205\234/\354\260\250\353\213\250\352\264\200\353\246\254.http" +++ /dev/null @@ -1,44 +0,0 @@ -### 유저 차단하기 -POST {{host}}/api/v1/blocks -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "blockedUserId": 2 -} - -### 차단 해제하기 -DELETE {{host}}/api/v1/blocks/2 -Authorization: Bearer {{accessToken}} - -### 차단한 유저 목록 조회 -GET {{host}}/api/v1/blocks -Authorization: Bearer {{accessToken}} - -### 차단한 유저 ID 목록 조회 -GET {{host}}/api/v1/blocks/ids -Authorization: Bearer {{accessToken}} - -### 특정 유저 차단 여부 확인 -GET {{host}}/api/v1/blocks/check/2 -Authorization: Bearer {{accessToken}} - -### 상호 차단 여부 확인 -GET {{host}}/api/v1/blocks/mutual-check/2 -Authorization: Bearer {{accessToken}} - -### 내가 차단한 유저 수 조회 -GET {{host}}/api/v1/blocks/stats/blocking-count -Authorization: Bearer {{accessToken}} - -### 나를 차단한 유저 수 조회 -GET {{host}}/api/v1/blocks/stats/blocked-by-count -Authorization: Bearer {{accessToken}} - -### 차단 필터링 테스트 - 리뷰 조회 -GET {{host}}/api/v1/reviews/274 -Authorization: Bearer {{accessToken}} - -### 차단 필터링 테스트 - 리뷰 상세 조회 -GET {{host}}/api/v1/reviews/detail/1 -Authorization: Bearer {{accessToken}} diff --git "a/http/product/04_\354\206\214\354\205\234/\355\214\224\353\241\234\354\232\260\352\264\200\353\246\254.http" "b/http/product/04_\354\206\214\354\205\234/\355\214\224\353\241\234\354\232\260\352\264\200\353\246\254.http" deleted file mode 100644 index b6ef10336..000000000 --- "a/http/product/04_\354\206\214\354\205\234/\355\214\224\353\241\234\354\232\260\352\264\200\353\246\254.http" +++ /dev/null @@ -1,29 +0,0 @@ -### 팔로우 요청 -POST {{host}}/api/v1/follow -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "followUserId": 1, - "status": "following" -} - -### 팔로우 해제 -POST {{host}}/api/v1/follow -Content-Type: application/json -Authorization: Bearer {{accessToken}} - -{ - "followUserId": 1, - "status": "unfollow" -} - -### 팔로잉 목록 조회 (내가 팔로우한 사람들) -@targetUserId = 8 -GET {{host}}/api/v1/follow/{{targetUserId}}/following-list?cursor=0&pageSize=50 -Authorization: Bearer {{accessToken}} - -### 팔로워 목록 조회 (나를 팔로우한 사람들) -@targetUserId = 8 -GET {{host}}/api/v1/follow/{{targetUserId}}/follower-list?cursor=0&pageSize=50 -Authorization: Bearer {{accessToken}} diff --git "a/http/product/04_\354\206\214\354\205\234/\355\236\210\354\212\244\355\206\240\353\246\254/\355\236\210\354\212\244\355\206\240\353\246\254\354\241\260\355\232\214.http" "b/http/product/04_\354\206\214\354\205\234/\355\236\210\354\212\244\355\206\240\353\246\254/\355\236\210\354\212\244\355\206\240\353\246\254\354\241\260\355\232\214.http" deleted file mode 100644 index 3cb5a8d15..000000000 --- "a/http/product/04_\354\206\214\354\205\234/\355\236\210\354\212\244\355\206\240\353\246\254/\355\236\210\354\212\244\355\206\240\353\246\254\354\241\260\355\232\214.http" +++ /dev/null @@ -1,8 +0,0 @@ -### 사용자 히스토리 목록 조회 -@targetUserId = 8 -GET {{host}}/api/v1/history/{{targetUserId}}?cursor=0&pageSize=50&sortType=LATEST&sortOrder=DESC -Authorization: Bearer {{accessToken}} - -### 위스키 조회 히스토리 -GET {{host}}/api/v1/history/view/alcohols -Authorization: Bearer {{accessToken}} diff --git "a/http/product/05_\354\247\200\354\233\220/\353\254\270\354\235\230\352\264\200\353\246\254/\353\254\270\354\235\230\352\264\200\353\246\254.http" "b/http/product/05_\354\247\200\354\233\220/\353\254\270\354\235\230\352\264\200\353\246\254/\353\254\270\354\235\230\352\264\200\353\246\254.http" deleted file mode 100644 index b5c9ac43e..000000000 --- "a/http/product/05_\354\247\200\354\233\220/\353\254\270\354\235\230\352\264\200\353\246\254/\353\254\270\354\235\230\352\264\200\353\246\254.http" +++ /dev/null @@ -1,36 +0,0 @@ -### 문의 등록 -POST {{host}}/api/v1/help -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "title": "앱 사용 중 오류 문의", - "content": "리뷰 작성 시 이미지 업로드가 되지 않습니다.", - "category": "BUG" -} - -### 문의 목록 조회 -GET {{host}}/api/v1/help?cursor=0&pageSize=10 -Authorization: Bearer {{accessToken}} - -### 문의 상세 조회 -@helpId = 1 -GET {{host}}/api/v1/help/{{helpId}} -Authorization: Bearer {{accessToken}} - -### 문의 수정 -@helpId = 1 -PATCH {{host}}/api/v1/help/{{helpId}} -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "title": "앱 사용 중 오류 문의 (수정)", - "content": "리뷰 작성 시 이미지 업로드가 되지 않습니다. 재시도해도 동일합니다.", - "category": "BUG" -} - -### 문의 삭제 -@helpId = 1 -DELETE {{host}}/api/v1/help/{{helpId}} -Authorization: Bearer {{accessToken}} diff --git "a/http/product/05_\354\247\200\354\233\220/\353\271\204\354\246\210\353\213\210\354\212\244\354\247\200\354\233\220/\353\271\204\354\246\210\353\213\210\354\212\244\354\247\200\354\233\220.http" "b/http/product/05_\354\247\200\354\233\220/\353\271\204\354\246\210\353\213\210\354\212\244\354\247\200\354\233\220/\353\271\204\354\246\210\353\213\210\354\212\244\354\247\200\354\233\220.http" deleted file mode 100644 index 13894f03e..000000000 --- "a/http/product/05_\354\247\200\354\233\220/\353\271\204\354\246\210\353\213\210\354\212\244\354\247\200\354\233\220/\353\271\204\354\246\210\353\213\210\354\212\244\354\247\200\354\233\220.http" +++ /dev/null @@ -1,42 +0,0 @@ -### 비즈니스 문의 등록 -POST {{host}}/api/v1/business-support -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "companyName": "위스키 전문점 ABC", - "contactPerson": "김철수", - "email": "contact@abc-whisky.com", - "phone": "02-1234-5678", - "content": "제품 등록 및 광고 문의드립니다.", - "category": "ADVERTISING" -} - -### 비즈니스 문의 목록 조회 -GET {{host}}/api/v1/business-support?cursor=0&pageSize=10 -Authorization: Bearer {{accessToken}} - -### 비즈니스 문의 상세 조회 -@businessId = 1 -GET {{host}}/api/v1/business-support/{{businessId}} -Authorization: Bearer {{accessToken}} - -### 비즈니스 문의 수정 -@businessId = 1 -PATCH {{host}}/api/v1/business-support/{{businessId}} -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "companyName": "위스키 전문점 ABC (수정)", - "contactPerson": "김철수", - "email": "contact@abc-whisky.com", - "phone": "02-1234-5679", - "content": "제품 등록 및 광고 문의드립니다. 추가 문의사항이 있습니다.", - "category": "ADVERTISING" -} - -### 비즈니스 문의 삭제 -@businessId = 1 -DELETE {{host}}/api/v1/business-support/{{businessId}} -Authorization: Bearer {{accessToken}} diff --git "a/http/product/99_\352\263\265\355\206\265/\354\225\261\354\240\225\353\263\264.http" "b/http/product/99_\352\263\265\355\206\265/\354\225\261\354\240\225\353\263\264.http" deleted file mode 100644 index e9b5e537e..000000000 --- "a/http/product/99_\352\263\265\355\206\265/\354\225\261\354\240\225\353\263\264.http" +++ /dev/null @@ -1,2 +0,0 @@ -### 앱 정보 조회 -GET {{host}}/api/v1/app-info diff --git "a/http/product/99_\352\263\265\355\206\265/\355\214\214\354\235\274\354\227\205\353\241\234\353\223\234.http" "b/http/product/99_\352\263\265\355\206\265/\355\214\214\354\235\274\354\227\205\353\241\234\353\223\234.http" deleted file mode 100644 index e6e10d799..000000000 --- "a/http/product/99_\352\263\265\355\206\265/\355\214\214\354\235\274\354\227\205\353\241\234\353\223\234.http" +++ /dev/null @@ -1,23 +0,0 @@ -### 토큰 발급 (일반) -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "dev.bottle-note@gmail.com", - "socialType": "GOOGLE", - "gender": "MALE", - "age": 25 -} - -> {% - //토큰을 전역변수로 저장 - client.global.set("accessToken", response.body.data.accessToken); -%} - -### 토큰 없이 이미지 업로드 url 요청시 실패한다. -GET {{host}}/api/v1/s3/presign-url?rootPath=review&uploadSize=1 - - -### 토큰을 가지고 이미지 업로드 url 요청시 성공한다. -GET {{host}}/api/v1/s3/presign-url?rootPath=review&uploadSize=3 -Authorization: Bearer {{accessToken}} diff --git a/http/product/README.md b/http/product/README.md deleted file mode 100644 index 7b405fb74..000000000 --- a/http/product/README.md +++ /dev/null @@ -1,284 +0,0 @@ -# HTTP API 테스트 파일 가이드 - -IntelliJ IDEA HTTP Client를 사용한 API 테스트 파일 모음입니다. - -## 📁 폴더 구조 및 분류 기준 - -### 사용자 여정(User Journey) 기반 구조 - -``` -http/ -├── 01_회원관리/ # 사용자 계정 생애주기 (가입 → 사용 → 탈퇴) -├── 02_위스키탐색/ # 위스키 발견 및 정보 탐색 -├── 03_리뷰_평가/ # 사용자 피드백 및 평가 -├── 04_소셜/ # 사용자 간 상호작용 -├── 05_지원/ # 고객 지원 및 비즈니스 문의 -└── 99_공통/ # 전역 기능 (파일, 앱정보 등) -``` - -## 📝 새 HTTP 파일 추가 가이드 - -### 1. 적절한 폴더 찾기 - -**질문으로 분류하기:** -- "사용자가 언제 이 기능을 사용하나?" → 사용자 여정 단계 파악 -- "이 기능의 주요 목적은?" → 도메인 카테고리 결정 - -**예시:** -``` -새 기능: "위스키 추천 받기" -└─ 질문: 위스키를 탐색하는 중? → 02_위스키탐색/ - └─ 세부: 위스키를 찾는 기능? → 위스키찾기/ - └─ 파일명: 추천조회.http -``` - -### 2. 파일 네이밍 규칙 - -**기본 원칙:** -- ✅ 한글 사용 (직관성 우선) -- ✅ 명사형 (동작보다 대상 중심) -- ✅ 간결하게 (2-4단어) - -**패턴별 네이밍:** - -| 패턴 | 파일명 예시 | 설명 | -|------|------------|------| -| **조회만** | `마이페이지.http` | 단순 조회 | -| **CRUD 통합** | `문의관리.http` | 생성/조회/수정/삭제 모두 포함 | -| **특정 작업** | `리뷰작성_수정_삭제.http` | CUD만 분리 | -| **관련 기능** | `카테고리_지역.http` | 관련 조회 통합 | - -**예시:** -``` -❌ create-review.http # 영문 -❌ 리뷰를_작성한다.http # 서술형 -❌ 리뷰_등록_수정_삭제_조회.http # 너무 김 -✅ 리뷰작성_수정_삭제.http # 명확하고 간결 -``` - -### 3. 파일 내용 작성 규칙 - -**기본 템플릿:** -```http -### [기능 설명] -[METHOD] {{host}}/api/v1/[path] -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "key": "value" -} -``` - -**변수 사용:** -```http -### 위스키 상세 조회 -@alcoholId = 123 -GET {{host}}/api/v1/alcohols/{{alcoholId}} -Authorization: Bearer {{accessToken}} -``` - -**여러 테스트 케이스:** -```http -### 정상 케이스 -POST {{host}}/api/v1/reviews -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "alcoholId": 1, - "content": "좋습니다" -} - -### 실패 케이스 - 필수값 누락 -POST {{host}}/api/v1/reviews -Authorization: Bearer {{accessToken}} -Content-Type: application/json - -{ - "alcoholId": 1 -} -``` - -## 🔧 환경 변수 설정 - -**http-client.env.json 사용:** -```json -{ - "local": { - "host": "http://localhost:8080", - "accessToken": "" - }, - "dev": { - "host": "https://api.development.bottle-note.com", - "accessToken": "" - } -} -``` - -**파일에서 사용:** -```http -GET {{host}}/api/v1/users/current -Authorization: Bearer {{accessToken}} -``` - -## 📂 폴더별 상세 설명 - -### 01_회원관리 -``` -가입_로그인/ # 계정 생성 및 인증 -내정보/ # 프로필, 마이보틀 등 개인 정보 -계정관리/ # 토큰, 탈퇴 등 계정 유지관리 -``` - -### 02_위스키탐색 -``` -위스키찾기/ # 검색, 추천 등 발견 기능 -위스키정보/ # 상세, 둘러보기 등 정보 확인 -나만의위스키/ # 찜하기, 조회기록 등 개인화 -``` - -### 03_리뷰_평가 -``` -리뷰/ # 리뷰 CRUD 및 좋아요 -댓글/ # 리뷰 댓글 관리 -별점/ # 별점 등록 및 조회 -신고/ # 부적절한 콘텐츠 신고 -``` - -### 04_소셜 -``` -[루트] # 팔로우, 차단 등 기본 상호작용 -히스토리/ # 사용자 활동 기록 -``` - -### 05_지원 -``` -문의관리/ # 고객 문의 및 지원 -비즈니스지원/ # B2B 문의 및 제휴 -``` - -### 99_공통 -``` -전역적으로 사용되는 기능 -- 파일 업로드 -- 앱 정보 조회 -- 푸시 알림 -``` - -## 💡 작성 팁 - -### 토큰 자동 저장 -```http -### 토큰 발급 -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "test@example.com" -} - -> {% - client.global.set("accessToken", response.body.data.accessToken); -%} -``` - -### 주석 활용 -```http -### 리뷰 작성 -# @no-cookie-jar # 쿠키 저장 방지 -POST {{host}}/api/v1/reviews -``` - -### 실패 케이스 명시 -```http -### 실패 케이스 - 권한 없음 -DELETE {{host}}/api/v1/reviews/999 -Authorization: Bearer {{accessToken}} -``` - -## 📋 체크리스트 - -새 HTTP 파일 추가 시 확인사항: - -- [ ] 올바른 폴더 위치인가? -- [ ] 파일명이 명확하고 간결한가? -- [ ] `{{host}}`, `{{accessToken}}` 환경변수를 사용하는가? -- [ ] 각 요청에 설명(`###`)이 있는가? -- [ ] 필수 헤더(Authorization, Content-Type)가 포함되어 있는가? -- [ ] 요청 body가 실제 API 스펙과 일치하는가? - -## 🔗 참고 링크 - -- [IntelliJ HTTP Client 공식 문서](https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html) -- [프로젝트 API 문서](../docs/api/) (존재하는 경우) - ---- - -## 현재 폴더 구조 - -``` -http/ -├── http-client.env.json -├── _인증.http -│ -├── 01_회원관리/ -│ ├── 가입_로그인/ -│ │ ├── 소셜회원가입.http -│ │ ├── 일반토큰발급.http -│ │ ├── 게스트토큰발급.http -│ │ ├── 인증v2.http -│ │ └── OAuth레거시.http -│ ├── 내정보/ -│ │ ├── 마이페이지.http -│ │ ├── 프로필수정.http -│ │ ├── 마이보틀.http -│ │ └── 마이보틀세부조회.http -│ └── 계정관리/ -│ ├── 토큰검증.http -│ ├── 디바이스토큰.http -│ ├── 유저리포트.http -│ └── 회원탈퇴.http -│ -├── 02_위스키탐색/ -│ ├── 위스키찾기/ -│ │ ├── 통합검색.http -│ │ ├── 큐레이션검색.http -│ │ └── 인기위스키.http -│ ├── 위스키정보/ -│ │ ├── 상세보기.http -│ │ ├── 둘러보기.http -│ │ └── 카테고리_지역.http -│ └── 나만의위스키/ -│ ├── 찜하기.http -│ └── 조회기록.http -│ -├── 03_리뷰_평가/ -│ ├── 리뷰/ -│ │ ├── 리뷰보기.http -│ │ ├── 리뷰작성_수정_삭제.http -│ │ └── 리뷰좋아요.http -│ ├── 댓글/ -│ │ └── 댓글관리.http -│ ├── 별점/ -│ │ └── 별점관리.http -│ └── 신고/ -│ └── 신고관리.http -│ -├── 04_소셜/ -│ ├── 팔로우관리.http -│ ├── 차단관리.http -│ └── 히스토리/ -│ └── 히스토리조회.http -│ -├── 05_지원/ -│ ├── 문의관리/ -│ │ └── 문의관리.http -│ └── 비즈니스지원/ -│ └── 비즈니스지원.http -│ -└── 99_공통/ - ├── 파일업로드.http - ├── 앱정보.http - └── 푸시알림.http -``` diff --git "a/http/product/_\354\235\270\354\246\235.http" "b/http/product/_\354\235\270\354\246\235.http" deleted file mode 100644 index 0fe76ec53..000000000 --- "a/http/product/_\354\235\270\354\246\235.http" +++ /dev/null @@ -1,15 +0,0 @@ -# @no-cookie-jar -### 토큰 발급 (일반) -POST {{host}}/api/v1/oauth/login -Content-Type: application/json - -{ - "email": "rlagusrl928@gmail.com", - "socialType": "KAKAO" -} - - -> {% - //토큰을 전역변수로 저장 - client.global.set("accessToken", response.body.data.accessToken); -%} diff --git a/http/product/http-client.env.json b/http/product/http-client.env.json deleted file mode 100644 index c7be5dc93..000000000 --- a/http/product/http-client.env.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "local": { - "host": "http://localhost:8080", - "accessToken": "" - }, - "dev": { - "host": "https://api.development.bottle-note.com/", - "accessToken": "" - }, - "prod": { - "host": "", - "accessToken": "" - } -} diff --git a/plan/batch-version-policy.md b/plan/complete/batch-version-policy.md similarity index 100% rename from plan/batch-version-policy.md rename to plan/complete/batch-version-policy.md diff --git a/plan/multi-code-curation-feed.md b/plan/complete/multi-code-curation-feed.md similarity index 100% rename from plan/multi-code-curation-feed.md rename to plan/complete/multi-code-curation-feed.md