Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.whereyouad.WhereYouAd.domains.advertisement.application.dto.request;

public class AdvertisementRequest {
}
Comment on lines +3 to +4
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

TODO 주석 누락 및 Java 17 record 패턴 미적용

AdvertisementResponse에는 // TODO: 대시보드 공통 응답값 정의 주석이 있는데, AdvertisementRequest에는 없습니다. 나중에 요청 필드를 정의해야 한다는 사실을 잊기 쉬우니 TODO를 추가해주세요.

또한 기존 코드베이스의 EmailRequest, SmsRequest처럼 요청 케이스별로 내부 record를 사용하는 패턴이 정착되어 있습니다. 필드를 추가할 때 동일한 패턴을 따라주세요.

♻️ 개선 예시
 public class AdvertisementRequest {
+    // TODO: 광고 요청 DTO 필드 정의
+
+    // 예시 — 요청 케이스별 내부 record:
+    // public record Search(String platform, String campaignName) {}
+    // public record Create(String campaignName, ...) {}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public class AdvertisementRequest {
}
public class AdvertisementRequest {
// TODO: 광고 요청 DTO 필드 정의
// 예시 — 요청 케이스별 내부 record:
// public record Search(String platform, String campaignName) {}
// public record Create(String campaignName, ...) {}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/advertisement/application/dto/request/AdvertisementRequest.java`
around lines 3 - 4, Add a TODO comment to AdvertisementRequest noting "대시보드 요청값
정의" (matching the style in AdvertisementResponse) and refactor the class to
follow the established pattern by declaring one or more internal Java 17 record
types for request variants (e.g., AdvertisementRequest.Create,
AdvertisementRequest.Update or AdvertisementRequest.Payload) so future fields
can be added as records consistent with EmailRequest and SmsRequest; ensure the
top-level AdvertisementRequest remains a holder for those internal records and
include the TODO above or inside the class to remind implementers to define
request fields later.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.whereyouad.WhereYouAd.domains.advertisement.application.dto.response;

public class AdvertisementResponse {
// TODO: 대시보드 공통 응답값 정의
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.whereyouad.WhereYouAd.domains.advertisement.application.mapper;

public class AdvertisementConverter {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.whereyouad.WhereYouAd.domains.advertisement.domain.constant;

public enum Grain {
HOURLY, DAILY
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.whereyouad.WhereYouAd.domains.advertisement.domain.constant;

public enum Provider {
GOOGLE, KAKAO, NAVER
// 추후 값 추가
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.whereyouad.WhereYouAd.domains.advertisement.domain.service;

public interface AdvertisementQueryService {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.whereyouad.WhereYouAd.domains.advertisement.domain.service;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class AdvertisementQueryServiceImpl implements AdvertisementQueryService{

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.whereyouad.WhereYouAd.domains.advertisement.exception;

import com.whereyouad.WhereYouAd.global.exception.AppException;
import com.whereyouad.WhereYouAd.global.exception.BaseErrorCode;

public class AdvertisementException extends AppException {
public AdvertisementException(BaseErrorCode code) {
super(code);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.whereyouad.WhereYouAd.domains.advertisement.exception.code;

import com.whereyouad.WhereYouAd.global.exception.BaseErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum AdvertisementErrorCode implements BaseErrorCode {

// TODO: 에러 코드 추가
;

private final HttpStatus httpStatus;
private final String code;
private final String message;
Comment on lines +10 to +17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

광고 도메인 에러코드가 비어 있어 예외 표준화가 동작하지 않습니다.

TODO 상태로 두면 AdvertisementException이 실사용 시 도메인 식별 가능한 에러코드를 제공하지 못합니다. 최소 공통 코드부터 채워두는 걸 권장합니다.

🔧 제안 수정안
 public enum AdvertisementErrorCode implements BaseErrorCode {
-
-    // TODO: 에러 코드 추가
-    ;
+    ADVERTISEMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "ADVERTISEMENT_404", "광고를 찾을 수 없습니다."),
+    ADVERTISEMENT_BAD_REQUEST(HttpStatus.BAD_REQUEST, "ADVERTISEMENT_400", "잘못된 광고 요청입니다."),
+    ADVERTISEMENT_FORBIDDEN(HttpStatus.FORBIDDEN, "ADVERTISEMENT_403", "광고 접근 권한이 없습니다.");
 
     private final HttpStatus httpStatus;
     private final String code;
     private final String message;
 }

As per coding guidelines src/main/java/**/*.java: "SOLID 원칙, 의존성 주입(DI), 예외 처리(GlobalExceptionHandler)가 적절한지 보라."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/advertisement/exception/code/AdvertisementErrorCode.java`
around lines 10 - 17, The AdvertisementErrorCode enum is empty causing
AdvertisementException and global error handling to lack domain-specific codes;
populate AdvertisementErrorCode with a minimal set of common error entries
(e.g., NOT_FOUND, INVALID_REQUEST, CONFLICT, INTERNAL_ERROR) each carrying the
HttpStatus, a unique String code (like "AD_0001"), and a human message, and
implement the BaseErrorCode methods (getHttpStatus, getCode, getMessage) so
AdvertisementException and the GlobalExceptionHandler can read standardized
values; update any constructors or usages in AdvertisementException that
reference AdvertisementErrorCode to use the new enum constants.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.whereyouad.WhereYouAd.domains.advertisement.persistence.entity;

import com.whereyouad.WhereYouAd.domains.advertisement.domain.constant.Provider;
import com.whereyouad.WhereYouAd.global.common.BaseEntity;
import jakarta.persistence.*;
import lombok.*;

import java.time.LocalDateTime;

@Entity
@Getter
@Table(name = "advertisement")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Advertisement extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ad_id")
private Long id;
Comment on lines +16 to +21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

외부 광고 플랫폼의 고유 ID(externalAdId)가 없으면 동기화가 불가능합니다.

현재 Advertisement 엔티티에는 Google/Kakao/Naver 각 플랫폼에서 부여한 고유 광고 ID를 저장하는 필드가 없습니다. 향후 외부 API로부터 광고 데이터를 동기화할 때, 어떤 플랫폼 광고가 어떤 DB 레코드에 해당하는지 매핑할 수 없게 됩니다.

예를 들어 Google Ads API는 각 광고에 고유한 ad_id를 제공하는데, 이 값이 없으면 중복 insert나 업데이트 대상 특정이 불가능합니다.

+    `@Column`(name = "external_ad_id", nullable = false)
+    private String externalAdId;  // 외부 플랫폼(Google/Kakao/Naver)에서 부여한 광고 ID
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/advertisement/persistence/entity/Advertisement.java`
around lines 16 - 21, The Advertisement entity lacks a field to store the
external platform ad identifier; add a new String field externalAdId to the
Advertisement class (with a `@Column`(name="external_ad_id", nullable=false) and a
unique index/constraint together with platform identifier if you support
multiple platforms) and update its getters/setters and constructors; ensure DB
mapping by adding the column annotation and, if needed, a uniqueness constraint
(e.g., unique on (platform, external_ad_id)) so synchronization routines can
reliably find or upsert records by Advertisement.externalAdId.


@Enumerated(EnumType.STRING)
@Column(name = "provider", nullable = false)
private Provider provider;

private LocalDateTime startDate;

private LocalDateTime endDate;

// @Enumerated(EnumType.STRING)
// @Column(name = "status", nullable = false)
// @ColumnDefault("'ON_GOING'")
// AdStatus 는 ON_GOING, PAUSED, OVER 가 존재
// join 연산으로 인한 성능 저하 우려 -> 일단은 주석 처리
// private AdStatus adStatus;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.whereyouad.WhereYouAd.domains.advertisement.persistence.entity;

import com.whereyouad.WhereYouAd.domains.advertisement.domain.constant.Grain;
import com.whereyouad.WhereYouAd.domains.advertisement.domain.constant.Provider;
import com.whereyouad.WhereYouAd.domains.project.persistence.entity.Project;
import com.whereyouad.WhereYouAd.global.common.BaseEntity;
import jakarta.persistence.*;
import lombok.*;

import java.math.BigDecimal;
import java.time.LocalDateTime;

@Entity
@Getter
@Table(name = "metric_fact")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class MetricFact extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "metric_fact_id")
private Long id;

@Enumerated(EnumType.STRING)
@Column(name = "grain", nullable = false)
private Grain grain; // 집계 단위(HOURLY, DAILY)

@Column(name = "time_bucket", nullable = false)
private LocalDateTime timeBucket; // 집계 단위 시작 시간
Comment on lines +30 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# 먼저 MetricFact.java 파일 확인
find . -name "MetricFact.java" -type f | head -5

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 171


🏁 Script executed:

# Java 파일 구조 파악
git ls-files "src/main/java/com/whereyouad/WhereYouAd/domains/advertisement/persistence/entity/"

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 169


🏁 Script executed:

# 전체 파일 목록으로 경로 확인
fd -t f "MetricFact.java"

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 169


🏁 Script executed:

# MetricFact.java 파일 내용 확인
cat -n src/main/java/com/whereyouad/WhereYouAd/domains/advertisement/persistence/entity/MetricFact.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 2201


🏁 Script executed:

# 다른 엔티티에서 LocalDateTime 또는 Instant 사용 패턴 확인
rg "private (LocalDateTime|Instant)" src/main/java/com/whereyouad/WhereYouAd/domains/advertisement/persistence/entity/ -A 2

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 456


🏁 Script executed:

# application.properties 또는 application.yml 파일 확인 (현재 설정 상태)
find . -name "application*.yml" -o -name "application*.properties" | head -10

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 109


🏁 Script executed:

# application.yml 파일 내용 확인
cat -n ./src/main/resources/application.yml

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 3899


🏁 Script executed:

# BaseEntity 확인 (createdAt, updatedAt 등이 어떻게 정의되어 있는지)
find . -name "BaseEntity.java" -type f

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 144


🏁 Script executed:

# 코드베이스에서 Instant 사용 여부 확인
rg "import java.time.Instant" src/main/java/ -l | head -10

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 55


🏁 Script executed:

# Spring Boot 버전 확인
find . -name "pom.xml" -o -name "build.gradle" | head -3

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 87


🏁 Script executed:

# BaseEntity 파일 내용 확인
cat -n ./src/main/java/com/whereyouad/WhereYouAd/global/common/BaseEntity.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 1002


🏁 Script executed:

# build.gradle에서 Spring Boot 버전 확인
cat -n ./build.gradle | head -30

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 856


🏁 Script executed:

# 코드베이스에서 타임존 관련 설정이 있는지 확인
rg "timezone|TimeZone|UTC" src/main/java/ -i

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 375


🏁 Script executed:

# LocalDateTime이 다른 엔티티에서도 사용되고 있는지 확인
rg "private LocalDateTime" src/main/java/com/whereyouad/WhereYouAd/domains/ --type java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 496


🏁 Script executed:

# MySQL TIMESTAMP 타입의 타임존 처리 확인 및 Spring Boot 3.5.9 호환성 검증
cat build.gradle | grep -A 20 "dependencies"

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 835


🏁 Script executed:

# 기존 데이터 구조 확인: DB 스키마에서 time_bucket이 어떤 타입인지
rg "CREATE TABLE|time_bucket" src/ --type java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 221


팩트 테이블의 시간 컬럼으로 LocalDateTime 대신 Instant 사용을 권장합니다

LocalDateTime은 타임존 정보가 없어서, 서버 JVM의 로컬 타임존에만 의존하게 됩니다. 현재 application.yml에 타임존 설정이 없으므로, Google, Kakao, Naver 등 서로 다른 플랫폼에서 수집한 광고 지표를 집계할 때 문제가 발생할 수 있습니다:

  • 서버 JVM 타임존이 변경되면 동일한 UTC 시각의 time_bucket이 다른 값으로 저장됨
  • DST(일광절약시간) 변경 시 데이터 손상 위험
  • 다중 리전 환경에서 같은 광고 이벤트가 다른 시간으로 기록될 수 있음
⏱️ 권장 수정 — Instant 사용
-import java.time.LocalDateTime;
+import java.time.Instant;

-    `@Column`(name = "time_bucket", nullable = false)
-    private LocalDateTime timeBucket; // 집계 단위 시작 시간
+    `@Column`(name = "time_bucket", nullable = false)
+    private Instant timeBucket; // 집계 단위 시작 시간 (UTC 기준)

그리고 application.yml에 아래 설정을 추가하면 JPA/Hibernate가 항상 UTC 기준으로 DB에 저장합니다:

spring:
  jpa:
    properties:
      hibernate:
        jdbc:
          time_zone: UTC

참고: BaseEntitycreatedAt, updatedAt도 같은 문제를 안고 있으니 함께 Instant로 변경하는 것을 검토해보세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Column(name = "time_bucket", nullable = false)
private LocalDateTime timeBucket; // 집계 단위 시작 시간
`@Column`(name = "time_bucket", nullable = false)
private Instant timeBucket; // 집계 단위 시작 시간 (UTC 기준)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/advertisement/persistence/entity/MetricFact.java`
around lines 30 - 31, Change the MetricFact.timeBucket field from LocalDateTime
to Instant to avoid JVM-local timezone issues: update the declaration in
MetricFact (timeBucket) and any getters/setters or mappings that reference it,
and likewise consider changing BaseEntity.createdAt and BaseEntity.updatedAt
from LocalDateTime to Instant to keep timestamps consistent; also ensure
JPA/Hibernate stores UTC by adding the hibernate.jdbc.time_zone=UTC property to
application.yml so DB persistence uses UTC.


// @Column(name = "dimension_type")
// private String dimensionType;
Comment on lines +33 to +34
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

주석 처리된 dimensionType 필드는 머지 전 정리가 필요합니다

PR 설명에서 dimensionType 필드의 필요성을 논의 중이라고 언급했는데, 결론이 나오면 완전히 삭제하거나 주석 없이 추가하는 방향으로 정리하는 것이 좋습니다. 주석 처리된 코드가 그대로 머지되면 나중에 누가 추가한 건지, 왜 있는지 파악하기 어려워집니다.

🗑️ 제거 예시
-//    `@Column`(name = "dimension_type")
-//    private String dimensionType;
-
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// @Column(name = "dimension_type")
// private String dimensionType;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/advertisement/persistence/entity/MetricFact.java`
around lines 33 - 34, The MetricFact class contains a commented-out field
dimensionType; decide whether this field is required and then either remove the
commented lines entirely or restore the field by uncommenting and adding proper
`@Column`(name = "dimension_type") private String dimensionType; (and update any
constructors/getters/setters or mapping code such as in MetricFact) so the
codebase does not contain orphaned commented code; ensure the choice is
reflected in the PR description.


@Column(name = "impressions")
private Long impressions; // 노출수

@Column(name = "clicks")
private Long clicks; // 클릭수

@Column(name = "conversions")
private Long conversions; // 전환수
Comment on lines +36 to +43
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

팩트 테이블의 수치 컬럼은 nullable = false로 강제하고 기본값 0을 권장합니다

impressions, clicks, conversions 모두 nullable로 정의되어 있는데, 광고 지표 팩트 테이블에서 null0은 의미가 완전히 다릅니다.

  • null: "데이터가 수집되지 않음 / 알 수 없음"
  • 0: "측정됐지만 수치가 없음"

현재 설계에서 집계 쿼리(SUM, AVG 등)를 실행하면 null 행이 있을 경우 예상과 다른 결과가 나올 수 있습니다. 비즈니스 규칙상 "항상 측정값이 있어야 한다"면 아래처럼 처리하세요.

🛡️ 제약 추가 예시
-    `@Column`(name = "impressions")
-    private Long impressions;
+    `@Column`(name = "impressions", nullable = false)
+    private Long impressions; // 기본값 0 처리는 서비스 레이어에서 담당

-    `@Column`(name = "clicks")
-    private Long clicks;
+    `@Column`(name = "clicks", nullable = false)
+    private Long clicks;

-    `@Column`(name = "conversions")
-    private Long conversions;
+    `@Column`(name = "conversions", nullable = false)
+    private Long conversions;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/advertisement/persistence/entity/MetricFact.java`
around lines 36 - 43, The numeric fact fields in MetricFact (impressions,
clicks, conversions) should be non-nullable and default to 0: update the `@Column`
annotations on MetricFact for impressions, clicks and conversions to include
nullable = false and a columnDefinition/default value of 0 (or use
`@Column`(nullable = false) plus initialize the fields to 0L), and add a DB
migration to alter the underlying table to set NOT NULL and default 0 so
existing rows are backfilled; ensure any constructors or builders for MetricFact
also initialize these fields to 0L to avoid NPEs.


@Column(name = "spend", precision = 18, scale = 2)
private BigDecimal spend; // 광고비

@Column(name = "revenue", precision = 18, scale = 2)
private BigDecimal revenue; // 매출

@Enumerated(EnumType.STRING)
@Column(name = "provider", nullable = false)
private Provider provider;

// 연관 관계
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "project_id")
private Project project;

//추가 연관관계 -> Advertisement
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ad_id", nullable = false)
private Advertisement advertisement;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.whereyouad.WhereYouAd.domains.advertisement.persistence.repository;

import com.whereyouad.WhereYouAd.domains.advertisement.persistence.entity.MetricFact;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MetricFactRepository extends JpaRepository<MetricFact, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.whereyouad.WhereYouAd.domains.advertisement.presentation;

import com.whereyouad.WhereYouAd.domains.advertisement.presentation.docs.AdvertisementControllerDocs;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
@RequestMapping("/api/advertisement")
public class AdvertisementController implements AdvertisementControllerDocs {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.whereyouad.WhereYouAd.domains.advertisement.presentation.docs;

public interface AdvertisementControllerDocs {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.whereyouad.WhereYouAd.domains.project.application.dto.request;

public class ProjectRequest {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.whereyouad.WhereYouAd.domains.project.application.dto.response;

public class ProjectResponse {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.whereyouad.WhereYouAd.domains.project.application.mapper;

public class ProjectConverter {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.whereyouad.WhereYouAd.domains.project.domain.service;

public interface ProjectService {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.whereyouad.WhereYouAd.domains.project.domain.service;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class ProjectServiceImpl implements ProjectService{

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.whereyouad.WhereYouAd.domains.project.exception;

import com.whereyouad.WhereYouAd.global.exception.AppException;
import com.whereyouad.WhereYouAd.global.exception.BaseErrorCode;

public class ProjectException extends AppException {
public ProjectException(BaseErrorCode code) {
super(code);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.whereyouad.WhereYouAd.domains.project.exception.code;

import com.whereyouad.WhereYouAd.global.exception.BaseErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum ProjectErrorCode implements BaseErrorCode {

;

private final HttpStatus httpStatus;
private final String code;
private final String message;
Comment on lines +10 to +16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

ProjectErrorCode에 enum 상수 정의가 필요합니다.

Line 10-12처럼 상수가 비어 있으면 ProjectException에서 사용할 오류 코드가 없어 예외 매핑이 불가능합니다. 최소한 대표 오류(예: NOT_FOUND, FORBIDDEN 등)를 정의해 주세요.

🔧 예시 추가안
 public enum ProjectErrorCode implements BaseErrorCode {
- 
-    ;
+    PROJECT_NOT_FOUND(HttpStatus.NOT_FOUND, "PROJECT_404", "프로젝트를 찾을 수 없습니다."),
+    PROJECT_FORBIDDEN(HttpStatus.FORBIDDEN, "PROJECT_403", "프로젝트 접근 권한이 없습니다.");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public enum ProjectErrorCode implements BaseErrorCode {
;
private final HttpStatus httpStatus;
private final String code;
private final String message;
public enum ProjectErrorCode implements BaseErrorCode {
PROJECT_NOT_FOUND(HttpStatus.NOT_FOUND, "PROJECT_404", "프로젝트를 찾을 수 없습니다."),
PROJECT_FORBIDDEN(HttpStatus.FORBIDDEN, "PROJECT_403", "프로젝트 접근 권한이 없습니다.");
private final HttpStatus httpStatus;
private final String code;
private final String message;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/project/exception/code/ProjectErrorCode.java`
around lines 10 - 16, ProjectErrorCode currently defines no enum constants, so
ProjectException cannot map to any error codes; add representative enum
constants (e.g., NOT_FOUND, FORBIDDEN, BAD_REQUEST, INTERNAL_ERROR) to
ProjectErrorCode that supply the required HttpStatus, code string and human
message, ensure the enum includes a constructor to populate the existing fields
(httpStatus, code, message) and exposes the accessor methods required by
BaseErrorCode so ProjectException can use them for exception mapping.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.whereyouad.WhereYouAd.domains.project.persistence.entity;

import com.whereyouad.WhereYouAd.domains.organization.persistence.entity.Organization;
import com.whereyouad.WhereYouAd.global.common.BaseEntity;
import jakarta.persistence.*;
import lombok.*;

@Entity
@Getter
@Table(name = "project")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Project extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "project_id")
private Long id;

@Column(name = "name", nullable = false)
private String name;

@Column(name = "description")
private String description;

@Column(name = "created_by", nullable = false)
private Long createdBy;

// 연관 관계
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "org_id")
private Organization organization;
Comment on lines +31 to +33
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

@JoinColumnnullable = false 제약이 없습니다

모든 Project는 반드시 Organization에 속해야 한다면, JPA 레벨에서도 이를 강제해야 합니다. 현재는 null organization을 가진 Project가 저장될 수 있습니다.

🛡️ 제약 추가 예시
 `@ManyToOne`(fetch = FetchType.LAZY)
-@JoinColumn(name = "org_id")
+@JoinColumn(name = "org_id", nullable = false)
 private Organization organization;

만약 비즈니스 규칙상 organization 없는 Project가 유효하다면 현재 코드가 맞습니다. 확인 후 필요 없다면 그대로 두세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "org_id")
private Organization organization;
`@ManyToOne`(fetch = FetchType.LAZY)
`@JoinColumn`(name = "org_id", nullable = false)
private Organization organization;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/project/persistence/entity/Project.java`
around lines 31 - 33, Project's organization association allows nulls at the JPA
level; add a non-null constraint on the `@JoinColumn` for the organization field
in the Project entity so JPA prevents persisting a Project without an
Organization. Locate the Project class and update the organization mapping (the
field named "organization" annotated with `@ManyToOne` and `@JoinColumn`) to include
nullable = false on `@JoinColumn`, and ensure any existing code that creates/saves
Project instances supplies a valid Organization (and adjust
tests/migrations/schema as needed).

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.whereyouad.WhereYouAd.domains.project.persistence.repository;

import com.whereyouad.WhereYouAd.domains.project.persistence.entity.Project;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProjectRepository extends JpaRepository<Project, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.whereyouad.WhereYouAd.domains.project.presentation;

import com.whereyouad.WhereYouAd.domains.project.presentation.docs.ProjectControllerDocs;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
@RequestMapping("/api/project")
public class ProjectController implements ProjectControllerDocs {
Comment on lines +8 to +11
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 프로젝트 내 모든 Java 컨트롤러의 `@RequiredArgsConstructor` 패턴 확인
echo "=== Searching for `@RequiredArgsConstructor` patterns in controllers ==="
rg -n "@RequiredArgsConstructor" --type java -A 2 -B 0

echo -e "\n=== Searching in presentation layer specifically ==="
find . -path "*/presentation/*" -name "*Controller.java" -type f | head -20

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 9900


@RequiredArgsConstructor(access = AccessLevel.PROTECTED) — 팀 컨트롤러와 일관성 맞춰주세요.

현재는 final 필드가 없어서 문제가 없지만, 나중에 서비스 필드(private final ProjectService projectService)가 추가되면 Lombok이 protected 접근자를 가진 생성자를 생성합니다.

코드베이스를 살펴보니 대부분의 컨트롤러(OrgController, UserController, AuthController)는 @RequiredArgsConstructor 기본값(PUBLIC)을 사용하고 있는데, ProjectController만 PROTECTED를 적용했네요. 일관성 측면에서:

  • Spring Framework 6(Boot 3.x)은 protected 생성자도 리플렉션으로 주입 가능합니다.
  • 하지만 CGLIB 기반 프록시가 필요한 일부 시나리오에서는 예기치 않은 문제가 발생할 수 있습니다.
  • 팀 표준(공개 생성자)에 맞춰서 @RequiredArgsConstructor 기본값으로 변경하거나, 팀 전체 컨트롤러에서 명시적으로 PROTECTED를 사용하도록 통일해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/project/presentation/ProjectController.java`
around lines 8 - 11, ProjectController currently uses
`@RequiredArgsConstructor`(access = AccessLevel.PROTECTED) which will generate a
protected constructor if a private final service (e.g. private final
ProjectService projectService) is added later; change this to the default public
constructor by removing the access attribute so the annotation is simply
`@RequiredArgsConstructor` to match other controllers (OrgController,
UserController, AuthController) and ensure consistent public constructor
behavior.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.whereyouad.WhereYouAd.domains.project.presentation.docs;

public interface ProjectControllerDocs {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.whereyouad.WhereYouAd.infrastructure.client.google.converter;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P4: infrastructure 에 추후 API 연동 위한 확장 구조 해놓은 거 좋은거 같습니다!!

public class GoogleConverter {
// GoogleDTO -> MetricFactResponse 변환
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.whereyouad.WhereYouAd.infrastructure.client.google.dto;

public class GoogleDTO {
// Google 응답 원문
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.whereyouad.WhereYouAd.infrastructure.client.kakao.converter;

public class KakaoConverter {
// KakaoDTO -> MetricFactResponse 변환
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.whereyouad.WhereYouAd.infrastructure.client.kakao.dto;

public class KakaoDTO {
// Kakao 응답 원문
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.whereyouad.WhereYouAd.infrastructure.client.naver.converter;

public class NaverConverter {
// NaverDTO -> MetricFactResponse 변환
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.whereyouad.WhereYouAd.infrastructure.client.naver.dto;

public class NaverDTO {
// Naver 응답 원문
}