diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml deleted file mode 100644 index 96bf0534..00000000 --- a/.github/workflows/ci-test.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: 핀하우스 테스트 CI 파이프라인 - -### develop으로 PR 올릴 때, 테스트 코드를 실행하여, 빌드 여부 및 테스트를 체크하는 로직을 수행한다. - -on: - pull_request: - branches: [ "develop"] - -jobs: - #1. 통합 테스트 용 - test: - - runs-on: ubuntu-22.04 - services: - - mysql: - image: mysql:8.0 - ports: - - '3306:3306' - env: - MYSQL_DATABASE: pinhouse_test - MYSQL_USER: testuser - MYSQL_PASSWORD: testpass - MYSQL_ROOT_PASSWORD: root - - redis: - image: redis:7.2.5 - ports: - - '6379:6379' - - mongo: - image: mongo:6.0 - ports: - - 27017:27017 - - permissions: - contents: write - checks: write - pull-requests: write - - steps: - # 1-1. repository checkout - - uses: actions/checkout@v4 - - # 1-2. jdk 환경 설치 - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - # 1-3. '*.yml' 파일 세팅 - - name: application.yml 파일 설정 - run: | - mkdir -p src/main/resources - echo "${{ secrets.APPLICATION_YML }}" > ./src/main/resources/application.yml - - - name: application-dev.yml 설정 - run: echo "${{ secrets.APPLICATION_DEV_YML }}" > ./src/main/resources/application-dev.yml - - - name: application-oauth2.yml 설정 - run: echo "${{ secrets.APPLICATION_OAUTH2_YML }}" > ./src/main/resources/application-oauth2.yml - - - name: application-test.yml 설정 - run: | - mkdir -p src/test/resources - echo "${{ secrets.APPLICATION_TEST_YML }}" > ./src/test/resources/application-test.yml - - - # 1-4. 성능 향상을 위한 Gradle 캐싱 - - name: Gradle Caching - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - gradle-${{ runner.os }} - - # 1-5. 테스트 컨테이너들 작동여부 체크 - - name: MySQL 체크 - run: | - until nc -z localhost 3306; do - echo "Waiting for MySQL..." - sleep 3 - done - - - - name: Redis 체크 - run: | - until nc -z localhost 6379; do - echo "Waiting for Redis..." - sleep 3 - done - - - name: MongoDB 체크 - run: | - until nc -z localhost 27017; do - echo "Waiting for MongoDB..." - sleep 3 - done - - # 2. 빌드 - - name: Build with Gradle Wrapper - run: ./gradlew clean build - - # 3. JUnit 테스트 결과 게시 - - name: Test 결과 출력 - uses: EnricoMi/publish-unit-test-result-action@v2 - if: always() - with: - junit_files: '**/build/test-results/test/TEST-*.xml' - github_token: ${{ secrets.GITHUB_TOKEN }} - diff --git a/.github/workflows/dev-ci-cd.yml b/.github/workflows/dev-ci-cd.yml index aa9e808c..150c3c96 100644 --- a/.github/workflows/dev-ci-cd.yml +++ b/.github/workflows/dev-ci-cd.yml @@ -119,7 +119,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - dockerfile: Dockerfile + dockerfile: Dockerfile-dev push: true tags: ${{secrets.DOCKER_USERNAME}}/server:latest diff --git a/.github/workflows/dev-ci-test.yml b/.github/workflows/dev-ci-test.yml new file mode 100644 index 00000000..a5ce06be --- /dev/null +++ b/.github/workflows/dev-ci-test.yml @@ -0,0 +1,88 @@ +name: 핀하우스 테스트 CI 파이프라인 + +on: + pull_request: + branches: [ "develop", "main" ] + +jobs: + test: + runs-on: ubuntu-22.04 + services: + mysql: + image: mysql:8.0 + ports: [ '3306:3306' ] + env: + MYSQL_DATABASE: pinhouse_test + MYSQL_USER: testuser + MYSQL_PASSWORD: testpass + MYSQL_ROOT_PASSWORD: root + redis: + image: redis:7.2.5 + ports: [ '6379:6379' ] + mongo: + image: mongo:6.0 + ports: [ 27017:27017 ] + + permissions: + contents: write + checks: write + pull-requests: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + # 1-3. 환경 설정 파일 세팅 + - name: Resources 디렉토리 생성 + run: | + mkdir -p src/main/resources + mkdir -p src/test/resources + + - name: 설정 파일 생성 (Secrets 활용) + run: | + echo "${{ secrets.APPLICATION_DEV_YML }}" > ./src/main/resources/application-dev.yml + echo "${{ secrets.APPLICATION_PROD_YML }}" > ./src/main/resources/application-prod.yml + echo "${{ secrets.APPLICATION_OAUTH2_YML }}" > ./src/main/resources/application-oauth2.yml + echo "${{ secrets.APPLICATION_TEST_YML }}" > ./src/test/resources/application-test.yml + + - name: Gradle Caching + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: gradle-${{ runner.os }} + + - name: 서비스 대기 (Health Check) + run: | + for port in 3306 6379 27017; do + until nc -z localhost $port; do + echo "Waiting for port $port..." + sleep 3 + done + done + + # 2. 브랜치에 따른 조건부 빌드 + # PR 대상이 main이면 prod 프로파일, 그 외(develop)는 dev 프로파일 사용 + - name: Build with Gradle Wrapper + run: | + if [ "${{ github.base_ref }}" == "main" ]; then + echo "Running Build for Main Branch (PROD profile)" + ./gradlew clean build -Dspring.profiles.active=prod + else + echo "Running Build for Develop Branch (DEV profile)" + ./gradlew clean build -Dspring.profiles.active=dev + fi + + - name: Test 결과 출력 + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + junit_files: '**/build/test-results/test/TEST-*.xml' + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/main-ci-cd.yml b/.github/workflows/main-ci-cd.yml new file mode 100644 index 00000000..68dc320c --- /dev/null +++ b/.github/workflows/main-ci-cd.yml @@ -0,0 +1,201 @@ +name: 핀하우스 prod CI-CD 파이프라인 + +### main으로 Merge 되었을 때, 테스트 코드를 실행하여, 빌드 및 배포하는 로직을 수행한다. + +on: + push: + branches: [ "main"] + +jobs: + #1. 운영 서버 CI, Build 용 + CI: + runs-on: ubuntu-22.04 + services: + mysql: + image: mysql:8.0 + ports: + - '3306:3306' + env: + MYSQL_DATABASE: pinhouse_test + MYSQL_USER: testuser + MYSQL_PASSWORD: testpass + MYSQL_ROOT_PASSWORD: root + + redis: + image: redis:7.2.5 + ports: + - '6379:6379' + + mongo: + image: mongo:6.0 + ports: + - 27017:27017 + + permissions: + contents: write + checks: write + pull-requests: write + + steps: + # 1. repository checkout + - uses: actions/checkout@v4 + + # 2. jdk 환경 설치 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + # 3. '*.yml' 파일 세팅 + - name: application.yml 파일 설정 + run: | + mkdir -p src/main/resources + + cat < ./src/main/resources/application.yml + spring: + profiles: + active: prod + include: + - oauth2-dev + EOF + + echo "${{ secrets.APPLICATION_PROD_YML }}" > ./src/main/resources/application-prod.yml + echo "${{ secrets.APPLICATION_OAUTH2_DEV_YML }}" > ./src/main/resources/application-oauth2-dev.yml + + mkdir -p src/test/resources + echo "${{ secrets.APPLICATION_TEST_YML }}" > ./src/test/resources/application-test.yml + + # 체크 + - name: MySQL 체크 + run: | + until nc -z localhost 3306; do + echo "Waiting for MySQL..." + sleep 3 + done + + + - name: Redis 체크 + run: | + until nc -z localhost 6379; do + echo "Waiting for Redis..." + sleep 3 + done + + - name: MongoDB 체크 + run: | + until nc -z localhost 27017; do + echo "Waiting for MongoDB..." + sleep 3 + done + + # 4. gradle 환경 설치 + - name: Gradle Wrapper 권한 부여 + run: chmod +x gradlew + + # 4-1. 캐싱 + - name: Gradle Caching + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + gradle-${{ runner.os }} + + - name: Gradle 빌드 + run: ./gradlew clean -Dspring.profiles.active=prod build + + # 5. JUnit 테스트 결과 게시 + - name: Test 결과 출력 + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + junit_files: '**/build/test-results/test/TEST-*.xml' + github_token: ${{ secrets.GITHUB_TOKEN }} + + #6. 도커 허브 로그인 + - name: Docker 로그인 + uses: docker/login-action@v3 + with: + username: ${{secrets.DOCKER_USERNAME}} + password: ${{secrets.DOCKER_ACCESS_TOKEN}} + + #7. 도커 이미지 Push + - name: Docker 이미지 Push + uses: docker/build-push-action@v5 + with: + context: . + dockerfile: Dockerfile-prod + push: true + tags: ${{secrets.DOCKER_USERNAME}}/prod-server:latest + + CD: + needs: CI + runs-on: ubuntu-22.04 + + steps: + - name: 1. Checkout source code + uses: actions/checkout@v3 + + - name: env 생성 + run: echo "${{ secrets.PROD_ENV }}" > .env + + - name: 3. docker-compose.yml 전달 + uses: appleboy/scp-action@master + with: + host: ${{ secrets.EC2_PROD_PUBLIC_IP }} + username: ${{ secrets.SSH_USER }} + key: ${{ secrets.EC2_PROD_PRIVATE_KEY }} + source: ".env,docker-compose.yml" + target: "/home/ubuntu/app/" + + - name: 4. EC2에서 docker-compose 실행 + uses: appleboy/ssh-action@master # SSH를 사용하여 EC2에서 명령 실행 + with: + host: ${{ secrets.EC2_PROD_PUBLIC_IP }} # EC2 퍼블릭 IP + username: ${{ secrets.SSH_USER }} + key: ${{ secrets.EC2_PROD_PRIVATE_KEY }} + script: | + # docker-compose 명령어 실행 + cd /home/ubuntu/app/ + docker compose pull spring + docker compose up -d spring + + notify-discord: + needs: + - CD + runs-on: ubuntu-latest + steps: + - name: 체크아웃 + uses: actions/checkout@v2 + + - name: 최신 커밋 추출하기 + id: commit-info + run: | + COMMIT_MSG=$(git log -1 --pretty=format:'%s') + COMMIT_AUTHOR=$(git log -1 --pretty=format:'%an') + COMMIT_HASH=$(git log -1 --pretty=format:'%h') + + echo "message=$COMMIT_MSG" >> $GITHUB_OUTPUT + echo "author=$COMMIT_AUTHOR" >> $GITHUB_OUTPUT + echo "hash=$COMMIT_HASH" >> $GITHUB_OUTPUT + + - name: 디스코드 알림 전송하기 + uses: sarisia/actions-status-discord@v1 + with: + webhook: ${{secrets.DISCORD_WEBHOOK_URL}} + title: "✅ 운영서버 배포" + description: | + **✅ 공지**: 운영 환경에 새로운 변경사항이 배포되었습니다. + + **배포 정보**: + - 커밋: `${{ github.sha }}` + - 배포자: ${{ github.actor }} + - 시간: ${{ github.event.head_commit.timestamp }} + + [커밋 보기](${{ github.event.head_commit.url }}) + color: 0x4287f5 + username: 핀하우스 GitHub Dev Bot + avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png diff --git a/Dockerfile b/Dockerfile-dev similarity index 100% rename from Dockerfile rename to Dockerfile-dev diff --git a/Dockerfile-prod b/Dockerfile-prod new file mode 100644 index 00000000..8deb5006 --- /dev/null +++ b/Dockerfile-prod @@ -0,0 +1,19 @@ +# ====== Build Stage ====== +FROM eclipse-temurin:21-jdk-alpine AS builder +# 작업 디렉토리를 /build로 설정합니다. +WORKDIR /build +# 파일을 이동시킵니다. +COPY . . +# 실행 가능한 Jar를 만듭니다. +RUN ./gradlew bootJar + +# ====== Runtime Stage ====== +FROM eclipse-temurin:21-jre-alpine +# 실행 파일 이동 +WORKDIR /app +# 빌드한 jar 파일 가져오기 +COPY --from=builder /build/build/libs/server-0.0.1-SNAPSHOT.jar app.jar +# 노출할 포트 +EXPOSE 8080 +# 실행하기 +CMD ["java", "-Dspring.profiles.active=prod", "-jar", "app.jar"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..cb70ce91 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,35 @@ +services: + + spring: + image: pinhouse/prod-server:latest + container_name: pinhouse-app + ports: + - "8080:8080" + env_file: + - .env # 여기서 .env 파일을 읽어 환경 변수로 주입 + environment: + - SPRING_PROFILES_ACTIVE=prod + networks: + - backend-bridge + + node-exporter: + image: prom/node-exporter:latest + container_name: node-exporter + restart: unless-stopped + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.rootfs=/rootfs' + - '--path.sysfs=/host/sys' + - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' + ports: + - "9100:9100" + networks: + - backend-bridge + +networks: + backend-bridge: + driver: bridge diff --git a/src/main/java/com/pinHouse/server/platform/housing/facility/application/dto/NoticeFacilityListResponse.java b/src/main/java/com/pinHouse/server/platform/housing/facility/application/dto/NoticeFacilityListResponse.java index d318d56c..c42a3547 100644 --- a/src/main/java/com/pinHouse/server/platform/housing/facility/application/dto/NoticeFacilityListResponse.java +++ b/src/main/java/com/pinHouse/server/platform/housing/facility/application/dto/NoticeFacilityListResponse.java @@ -12,7 +12,7 @@ @Builder @Schema(name = "[응답][시설] 주변 인프라 시설 목록", description = "단지 주변 1KM 이내의 주요 생활편의 시설 정보") public record NoticeFacilityListResponse( - @Schema(description = "주변 인프라 시설 타입 목록 (3개 이상 있는 시설만 포함)", example = "[\"PARK\", \"LIBRARY\", \"HOSPITAL\"]") + @Schema(description = "주변 인프라 시설 타입 목록 (3개 이상 있는 시설만 포함)", example = "[\"문화센터\", \"병원-약국\"]") List infra ) { /// 정적 팩토리 메서드 @@ -23,7 +23,8 @@ public static NoticeFacilityListResponse from(Map src) { List infraList = src.entrySet().stream() .filter(e -> e.getValue() != null && e.getValue() >= 3) - .map(Map.Entry::getKey) + .map(entry -> entry.getKey().displayType()) + .distinct() .toList(); return NoticeFacilityListResponse.builder() diff --git a/src/main/java/com/pinHouse/server/platform/housing/facility/application/service/FacilityService.java b/src/main/java/com/pinHouse/server/platform/housing/facility/application/service/FacilityService.java index ef117439..d773cbc9 100644 --- a/src/main/java/com/pinHouse/server/platform/housing/facility/application/service/FacilityService.java +++ b/src/main/java/com/pinHouse/server/platform/housing/facility/application/service/FacilityService.java @@ -111,7 +111,8 @@ public List getFacilities(String complexId) { /// 3개 이상인 FacilityType return response.entrySet().stream() .filter(entry -> entry.getValue() != null && entry.getValue() >= 3) - .map(Map.Entry::getKey) + .map(entry -> entry.getKey().displayType()) + .distinct() .toList(); } diff --git a/src/main/java/com/pinHouse/server/platform/housing/facility/application/service/FacilityStatService.java b/src/main/java/com/pinHouse/server/platform/housing/facility/application/service/FacilityStatService.java index 4baccf53..e67e0102 100644 --- a/src/main/java/com/pinHouse/server/platform/housing/facility/application/service/FacilityStatService.java +++ b/src/main/java/com/pinHouse/server/platform/housing/facility/application/service/FacilityStatService.java @@ -5,19 +5,14 @@ import com.pinHouse.server.platform.housing.facility.domain.repository.FacilityStatDocumentRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.bson.Document; -import org.springframework.data.geo.Metrics; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.aggregation.*; -import org.springframework.data.mongodb.core.geo.GeoJsonPoint; -import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.core.query.NearQuery; -import org.springframework.data.mongodb.core.query.Query; import org.springframework.stereotype.Service; import java.time.Duration; import java.time.Instant; -import java.util.*; +import java.util.Collection; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; @Service @RequiredArgsConstructor @@ -42,20 +37,20 @@ public Map getCountsOrRecompute(String complexId, double // 문서가 아예 없을 경우 → 새로 계산 if (existing == null) { - Map fresh = countsRepo.aggregateCounts(lng, lat, RADIUS_M); + Map fresh = withDerivedCounts(countsRepo.aggregateCounts(lng, lat, RADIUS_M)); upsertCounts(complexId, fresh); return fresh; } // TTL 지나면 재계산 if (isExpired(existing.getUpdatedAt())) { - Map fresh = countsRepo.aggregateCounts(lng, lat, RADIUS_M); + Map fresh = withDerivedCounts(countsRepo.aggregateCounts(lng, lat, RADIUS_M)); upsertCounts(complexId, fresh); return fresh; } // 유효한 캐시면 그대로 사용 - return existing.getCounts(); + return withDerivedCounts(existing.getCounts()); } @@ -63,7 +58,7 @@ private void upsertCounts(String complexId, Map counts) { FacilityStatDocument doc = FacilityStatDocument.builder() .id(complexId) .radiusKm(RADIUS_KM) - .counts(counts) + .counts(withDerivedCounts(counts)) .updatedAt(Instant.now()) .build(); countsRepo.save(doc); @@ -72,4 +67,23 @@ private void upsertCounts(String complexId, Map counts) { private boolean isExpired(Instant t) { return t == null || t.isBefore(Instant.now().minus(TTL)); } + + private Map withDerivedCounts(Map counts) { + if (counts == null) { + return Map.of(); + } + + Map enriched = new EnumMap<>(FacilityType.class); + for (FacilityType type : FacilityType.values()) { + enriched.put(type, counts.getOrDefault(type, 0)); + } + enriched.put(FacilityType.CULTURE_CENTER, computeCultureCenterCount(enriched)); + return enriched; + } + + private int computeCultureCenterCount(Map map) { + return FacilityType.cultureCenterMembers().stream() + .mapToInt(t -> map.getOrDefault(t, 0)) + .sum(); + } } diff --git a/src/main/java/com/pinHouse/server/platform/housing/facility/domain/entity/FacilityType.java b/src/main/java/com/pinHouse/server/platform/housing/facility/domain/entity/FacilityType.java index 242132ba..9983a534 100644 --- a/src/main/java/com/pinHouse/server/platform/housing/facility/domain/entity/FacilityType.java +++ b/src/main/java/com/pinHouse/server/platform/housing/facility/domain/entity/FacilityType.java @@ -4,22 +4,29 @@ import com.fasterxml.jackson.annotation.JsonValue; import com.pinHouse.server.core.exception.code.FacilityErrorCode; import com.pinHouse.server.core.response.response.CustomException; -import com.pinHouse.server.core.response.response.ErrorCode; import lombok.RequiredArgsConstructor; +import java.util.EnumSet; +import java.util.Locale; +import java.util.Set; + @RequiredArgsConstructor public enum FacilityType { - LIBRARY("도서관"), // 도서관 - PARK("공원"), // 공원 - ANIMAL("동물 관련시설"), // 동물 관련 시설 - WALKING("산책로"), // 산책로 - SPORT("스포츠 시설"), // 스포츠 시설 - STORE("대형점포"), // 대형점포 - HOSPITAL("병원"), // 병원 - EXHIBITION("전시회"), // 전시회 - LAUNDRY("빨래방"); // 빨래방 + LIBRARY("도서관", Set.of("도서관", "문화센터")), + PARK("공원", Set.of("공원", "문화센터")), + ANIMAL("동물 관련시설", Set.of("동물 관련시설", "문화센터")), + EXHIBITION("전시회", Set.of("전시회", "문화센터")), + + WALKING("산책길", Set.of("산책길", "산책로")), + SPORT("실내 액티비티", Set.of("실내 액티비티", "스포츠 시설")), + STORE("대형마트-백화점", Set.of("대형마트-백화점", "대형점포")), + HOSPITAL("병원-약국", Set.of("병원-약국", "병원")), + LAUNDRY("세탁소", Set.of("세탁소", "빨래방")), + CULTURE_CENTER("문화센터", Set.of("문화센터")); + private final String value; + private final Set aliases; @JsonValue public String getValue() { @@ -28,12 +35,53 @@ public String getValue() { @JsonCreator public static FacilityType fromValue(String value) { + if (value == null) { + throw new CustomException(FacilityErrorCode.BAD_REQUEST_INPUT_FACILITY); + } + + String normalized = normalize(value); + + if (CULTURE_CENTER.matches(normalized) || + FacilityType.cultureCenterMembers().stream().anyMatch(t -> t.matches(normalized))) { + return CULTURE_CENTER; + } + for (FacilityType facilityType : FacilityType.values()) { - if (facilityType.getValue().equals(value)) { + if (facilityType == CULTURE_CENTER) continue; + if (facilityType.matches(normalized)) { return facilityType; } } + + try { + return FacilityType.valueOf(normalized.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ignore) { } + throw new CustomException(FacilityErrorCode.BAD_REQUEST_INPUT_FACILITY); } + public FacilityType displayType() { + return isCultureCenterMember(this) ? CULTURE_CENTER : this; + } + + private boolean matches(String source) { + String normalized = normalize(source); + return aliases.stream().anyMatch(alias -> normalize(alias).equals(normalized)) + || normalize(getValue()).equals(normalized); + } + + private static String normalize(String input) { + return input == null ? "" : input.trim().replaceAll("\\s+", ""); + } + + public static boolean isCultureCenterMember(FacilityType type) { + return CULTURE_TYPES.contains(type); + } + + public static Set cultureCenterMembers() { + return CULTURE_TYPES; + } + + private static final Set CULTURE_TYPES = EnumSet.of(LIBRARY, PARK, ANIMAL, EXHIBITION); + } diff --git a/src/main/java/com/pinHouse/server/platform/housing/facility/domain/repository/FacilityStatDocumentRepositoryImpl.java b/src/main/java/com/pinHouse/server/platform/housing/facility/domain/repository/FacilityStatDocumentRepositoryImpl.java index 3adff23d..46cd125f 100644 --- a/src/main/java/com/pinHouse/server/platform/housing/facility/domain/repository/FacilityStatDocumentRepositoryImpl.java +++ b/src/main/java/com/pinHouse/server/platform/housing/facility/domain/repository/FacilityStatDocumentRepositoryImpl.java @@ -32,7 +32,17 @@ public List findByAllTypesOver(Collection ty } List ands = types.stream() - .map(t -> Criteria.where("counts." + t.name()).gte(min)) + .map(t -> { + if (t == FacilityType.CULTURE_CENTER) { + List ors = new ArrayList<>(); + ors.add(Criteria.where("counts." + FacilityType.CULTURE_CENTER.name()).gte(min)); + FacilityType.cultureCenterMembers().forEach(member -> + ors.add(Criteria.where("counts." + member.name()).gte(min)) + ); + return new Criteria().orOperator(ors.toArray(new Criteria[0])); + } + return Criteria.where("counts." + t.name()).gte(min); + }) .toList(); Query q = new Query(new Criteria().andOperator(ands.toArray(new Criteria[0]))); @@ -87,6 +97,7 @@ public Map aggregateCounts(double lng, double lat, double for (FacilityType t : FacilityType.values()) { map.putIfAbsent(t, 0); } + map.put(FacilityType.CULTURE_CENTER, computeCultureCenterCount(map)); return map; } @@ -95,7 +106,14 @@ private Map initEmptyMap() { for (FacilityType t : FacilityType.values()) { empty.put(t, 0); } + empty.put(FacilityType.CULTURE_CENTER, 0); return empty; } + private int computeCultureCenterCount(Map map) { + return FacilityType.cultureCenterMembers().stream() + .mapToInt(t -> map.getOrDefault(t, 0)) + .sum(); + } + } diff --git a/src/main/java/com/pinHouse/server/platform/housing/facility/presentation/FacilityTypeConverter.java b/src/main/java/com/pinHouse/server/platform/housing/facility/presentation/FacilityTypeConverter.java index b6bbcb4d..116b223d 100644 --- a/src/main/java/com/pinHouse/server/platform/housing/facility/presentation/FacilityTypeConverter.java +++ b/src/main/java/com/pinHouse/server/platform/housing/facility/presentation/FacilityTypeConverter.java @@ -16,18 +16,6 @@ public FacilityType convert(String source) { if (source == null) { throw new CustomException(FacilityErrorCode.BAD_REQUEST_INPUT_FACILITY); } - String s = source.trim(); - - /// 한글 라벨 매칭 - for (FacilityType t : FacilityType.values()) { - /// getValue()는 "공원", "도서관" 등 - if (t.getValue().equals(s)) return t; - } - /// 영문 enum 이름 매칭도 허용하고 싶으면: - try { - return FacilityType.valueOf(s.toUpperCase(Locale.ROOT)); - } catch (IllegalArgumentException ignore) {} - - throw new CustomException(FacilityErrorCode.BAD_REQUEST_INPUT_FACILITY); + return FacilityType.fromValue(source.trim()); } } diff --git a/src/main/java/com/pinHouse/server/platform/housing/notice/application/dto/NoticeDetailFilterRequest.java b/src/main/java/com/pinHouse/server/platform/housing/notice/application/dto/NoticeDetailFilterRequest.java index b0233989..aa453470 100644 --- a/src/main/java/com/pinHouse/server/platform/housing/notice/application/dto/NoticeDetailFilterRequest.java +++ b/src/main/java/com/pinHouse/server/platform/housing/notice/application/dto/NoticeDetailFilterRequest.java @@ -36,7 +36,7 @@ public record NoticeDetailFilterRequest( @Schema(description = "주택형 코드 필터", example = "[\"26A\"]") List typeCode, - @Schema(description = "원하는 인프라, 최대 3개까지 가능", example = "[\"공원\"]") + @Schema(description = "원하는 인프라, 최대 3개까지 가능", example = "[\"문화센터\"]") @Size(max = 3) List facilities diff --git a/src/main/java/com/pinHouse/server/platform/housing/notice/application/dto/UnitTypeCompareResponse.java b/src/main/java/com/pinHouse/server/platform/housing/notice/application/dto/UnitTypeCompareResponse.java index 863fed6d..76e2722a 100644 --- a/src/main/java/com/pinHouse/server/platform/housing/notice/application/dto/UnitTypeCompareResponse.java +++ b/src/main/java/com/pinHouse/server/platform/housing/notice/application/dto/UnitTypeCompareResponse.java @@ -49,7 +49,7 @@ public record UnitTypeComparisonItem( @Schema(description = "비용 정보") CostInfo cost, - @Schema(description = "단지 기반 주변 인프라 태그 (Complex에 속한 시설 정보)", example = "[\"공원\", \"도서관\", \"병원\"]") + @Schema(description = "단지 기반 주변 인프라 태그 (Complex에 속한 시설 정보)", example = "[\"문화센터\", \"병원-약국\"]") List nearbyFacilities, @Schema(description = "핀포인트 기준 대중교통 총 소요 시간", example = "1시간 30분") diff --git a/src/main/java/com/pinHouse/server/platform/search/application/dto/FastSearchRequest.java b/src/main/java/com/pinHouse/server/platform/search/application/dto/FastSearchRequest.java index 05bc8487..4d07865b 100644 --- a/src/main/java/com/pinHouse/server/platform/search/application/dto/FastSearchRequest.java +++ b/src/main/java/com/pinHouse/server/platform/search/application/dto/FastSearchRequest.java @@ -36,7 +36,7 @@ public record FastSearchRequest( @Schema(description = "월 임대료 최대값", example = "300000") Integer maxMonthPay, - @Schema(description = "원하는 인프라, 최대 3개까지 가능", example = "[\"도서관\"]") + @Schema(description = "원하는 인프라, 최대 3개까지 가능", example = "[\"문화센터\"]") @Size(max = 3) List facilities, diff --git a/src/main/java/com/pinHouse/server/platform/user/application/dto/UpdateFacilityTypesRequest.java b/src/main/java/com/pinHouse/server/platform/user/application/dto/UpdateFacilityTypesRequest.java index bb70dce3..b6d7a4ea 100644 --- a/src/main/java/com/pinHouse/server/platform/user/application/dto/UpdateFacilityTypesRequest.java +++ b/src/main/java/com/pinHouse/server/platform/user/application/dto/UpdateFacilityTypesRequest.java @@ -9,7 +9,7 @@ @Schema(description = "관심 시설 타입 수정 요청") public record UpdateFacilityTypesRequest( - @Schema(description = "관심 시설 타입 목록", example = "[\"도서관\", \"공원\", \"병원\"]") + @Schema(description = "관심 시설 타입 목록", example = "[\"문화센터\", \"병원-약국\", \"실내 액티비티\"]") @NotNull(message = "관심 시설 타입 목록은 필수입니다") List facilityTypes ) { diff --git a/src/main/java/com/pinHouse/server/platform/user/application/dto/UserRequest.java b/src/main/java/com/pinHouse/server/platform/user/application/dto/UserRequest.java index edee7375..1d002770 100644 --- a/src/main/java/com/pinHouse/server/platform/user/application/dto/UserRequest.java +++ b/src/main/java/com/pinHouse/server/platform/user/application/dto/UserRequest.java @@ -7,7 +7,7 @@ @Schema(name = "[요청][사용자] 사용자 요청", description = "사용자 요청을 위한 DTO입니다.") public record UserRequest( - @Schema(description = "사용자가 선택한 시설 유형 목록", example = "[\"도서관\", \"산책로\"]") + @Schema(description = "사용자가 선택한 시설 유형 목록", example = "[\"문화센터\", \"산책길\"]") List facilityTypes ) {