Conversation
Walkthrough조직 생성(Create) API를 구현합니다. 요청/응답 DTO, 데이터 변환 로직, 예외 처리, REST 컨트롤러, 서비스 인터페이스 및 구현을 추가하며, 사용자별 조직명 중복 검증 기능을 포함합니다. Read/Update/Delete 메서드는 스켈레톤 코드로 구성됩니다. Changes
Sequence DiagramsequenceDiagram
participant Client
participant OrgController
participant OrgServiceImpl
participant OrgMemberRepository
participant OrgRepository
participant Database
Client->>OrgController: POST /api/org/create<br/>(name, description, logoUrl)
OrgController->>OrgServiceImpl: createOrganization(userId, request)
OrgServiceImpl->>Database: UserRepository.findById(userId)
Database-->>OrgServiceImpl: User object
OrgServiceImpl->>OrgMemberRepository: findOrgMemberByUser(user)
OrgMemberRepository->>Database: Query OrgMembers by user
Database-->>OrgMemberRepository: List<OrgMember>
OrgServiceImpl->>OrgServiceImpl: validateNoDuplicateOrgName<br/>(orgMembers, requestName)
alt Duplicate Found
OrgServiceImpl->>OrgServiceImpl: throw OrgHandler<br/>(ORG_NAME_DUPLICATE)
OrgServiceImpl-->>OrgController: OrgHandler exception
OrgController-->>Client: 400 Bad Request
else No Duplicate
OrgServiceImpl->>OrgRepository: save(organization)
OrgRepository->>Database: INSERT Organization
Database-->>OrgRepository: orgId
OrgServiceImpl->>OrgRepository: save(orgMember)
OrgRepository->>Database: INSERT OrgMember
Database-->>OrgRepository: Success
OrgServiceImpl->>OrgServiceImpl: toCreatedResponse(org)
OrgServiceImpl-->>OrgController: OrgResponse.Create
OrgController-->>Client: 200 OK + {orgId, createdAt}
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45분 주요 검토 포인트:
Possibly related PRs
Suggested reviewers
리뷰 시 특히 살펴볼 점: 1️⃣ DB 쿼리 최적화 (중요) List<OrgMember> orgMembers = orgMemberRepository.findOrgMemberByUser(user);
// 이후 메모리에서 스트림 필터링으로 중복 검사현재는 사용자의 모든 조직멤버십을 메모리로 끌어와서 체크하는데, 사용자가 10개 이상 조직에 속해있으면 불필요한 데이터 로드가 발생합니다. 추천: 2️⃣ 입력 검증
3️⃣ 응답 일관성
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Important Action Needed: IP Allowlist UpdateIf your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:
Failure to add the new IP will result in interrupted reviews. Comment |
|
Swagger 를 보니까 조직 조회, 수정, 삭제에 대한 API 스켈레톤 코드들도 자동 인식이 되서 나타나는것 같네요ㅠㅠ 혼동을 막기위해 Controller 쪽 스켈레톤 코드는 주석처리 하겠습니다. |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java`:
- Around line 5-9: The Create record in OrgRequest lacks validation annotations
so empty/null values pass through; add `@NotBlank` to the name component of the
OrgRequest.Create record (and import javax.validation.constraints.NotBlank) so
Spring's `@Valid` triggers MethodArgumentNotValidException consistently;
optionally mirror patterns used in SignUpRequest/LoginRequest if you want
additional constraints (e.g., `@Size` or `@NotNull`) for description/logoUrl.
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/mapper/OrgConverter.java`:
- Around line 10-11: The toCreatedResponse method in OrgConverter should return
the entity's persisted created timestamp instead of LocalDateTime.now(); change
the returned createdAt to use organization.getCreatedAt() (or
organization.getCreatedAt().orElse(LocalDateTime.now()) / null-check fallback if
the field can be null) when constructing OrgResponse.Create so the response
reflects the DB-stored creation time; update OrgConverter.toCreatedResponse and
ensure OrgResponse.Create still accepts that timestamp type.
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgCRUDService.java`:
- Around line 41-49: The loop in OrgCRUDService that fetches
orgMemberRepository.findOrgMemberByUser(user) and inspects
orgMember.getOrganization().getName() causes N+1 queries and has a race
condition; replace that logic by adding a repository method boolean
existsByUserAndOrganization_Name(User user, String orgName) in
OrgMemberRepository and use
orgMemberRepository.existsByUserAndOrganization_Name(user, request.name()) to
perform a single query for the duplicate check; to prevent concurrent duplicate
creations also either apply a pessimistic lock on the enclosing service method
(e.g., `@Lock`(LockModeType.PESSIMISTIC_WRITE) on the transaction) or enforce a
DB-level unique constraint on (user_id, name).
- Around line 74-87: The three unimplemented methods getOrganization(Long
userId), modifyOrganization(Long userId, Long orgId, OrgRequest.Update request),
and removeOrganization(Long userId, Long orgId) currently return null/are empty
and cause misleading 200/204 responses; update them to throw a clear
not-implemented exception by throwing new
AppException(ErrorCode.NOT_IMPLEMENTED). If ErrorCode lacks NOT_IMPLEMENTED, add
NOT_IMPLEMENTED with HTTP 501 semantics to the ErrorCode enum and ensure
GlobalExceptionHandler maps AppException(ErrorCode.NOT_IMPLEMENTED) to an HTTP
501 response so clients receive a proper 501 Not Implemented for these methods.
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java`:
- Around line 19-24: The `@ApiResponses` on OrgControllerDocs.createOrganization
uses a non-standard responseCode "400_1"; change it to a valid HTTP status code
(e.g., "400") and add a "401" ApiResponse to document authentication failure;
keep any fine-grained error identifiers in the response body (not in
responseCode). Update the `@ApiResponse` entries for createOrganization (and other
controllers that follow the same pattern) to use standard numeric HTTP status
strings and ensure the response schema/body contains the project-specific error
code/details instead of using "400_1" style codes.
🧹 Nitpick comments (3)
src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java (1)
11-12: 중복 이름 검증은exists쿼리로 단일 SQL 처리하는 편이 안전합니다.서비스에서
OrgMember목록을 모두 조회한 뒤 조직명을 비교하면,OrgMember -> Organization이 LAZY일 때 N+1이 발생할 수 있어요. 중복 여부만 필요하다면 DB에서 바로 체크하는 메서드를 추가하는 편이 비용이 적습니다.🛠️ 제안 변경
+ boolean existsByUserAndOrganization_Name(User user, String name);필드명이 다르면
Organization경로는 실제 엔티티명에 맞게 조정해주세요.As per coding guidelines, JPA 사용 시 N+1 문제나 불필요한 쿼리가 발생하지 않는지, FetchType.LAZY가 잘 적용되었는지 체크하라.
src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java (2)
21-30: 생성 응답은 201을 고려해 주세요현재 200 OK인데, 생성 성공을 더 명확히 하려면 201 Created가 REST 관점에서 직관적입니다(필요하면 Location 헤더도 추가 가능).
🔧 제안 변경
- return ResponseEntity.ok( - DataResponse.created(response) - ); + return ResponseEntity.status(HttpStatus.CREATED) + .body(DataResponse.created(response));+import org.springframework.http.HttpStatus;
42-47: Update DTO에 제약 추가 시@Valid도 함께 붙여주세요Update 필드에 Bean Validation을 붙일 예정이라면 여기에도
@Valid가 있어야 검증이 동작합니다.🔧 제안 변경
public ResponseEntity<Void> modifyOrganization( `@AuthenticationPrincipal`(expression = "userId") Long userId, `@PathVariable` Long orgId, - `@RequestBody` OrgRequest.Update request + `@RequestBody` `@Valid` OrgRequest.Update request )
.../java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java
Show resolved
Hide resolved
...ain/java/com/whereyouad/WhereYouAd/domains/organization/application/mapper/OrgConverter.java
Outdated
Show resolved
Hide resolved
| //만약 해당 User 가 이미 같은 name 을 가진 Organization 에 속해있으면 예외처리 | ||
| //해당 User 의 OrgMember 를 모두 추출해서, | ||
| List<OrgMember> orgMemberByUser = orgMemberRepository.findOrgMemberByUser(user); | ||
| for (OrgMember orgMember : orgMemberByUser) { | ||
| //OrgMember 내부 Organization 의 name 이 생성하려는 request 의 name 과 같으면 | ||
| if (orgMember.getOrganization().getName().equals(request.name())) { | ||
| throw new OrgHandler(OrgErrorCode.ORG_NAME_DUPLICATE); //예외처리 | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "OrgCRUDService.java" -o -name "OrgMemberRepository.java" -o -name "OrgMember.java" | head -20Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 378
🏁 Script executed:
cat -n ./src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgCRUDService.javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 4292
🏁 Script executed:
cat -n ./src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 656
🏁 Script executed:
cat -n ./src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/entity/OrgMember.javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 1342
N+1 쿼리와 동시성 경합 문제가 확인되었습니다
현재 코드는 사용자의 모든 OrgMember를 먼저 조회한 후 루프에서 orgMember.getOrganization().getName()을 호출합니다. organization 필드가 FetchType.LAZY로 설정되어 있으므로, 사용자가 10개 조직에 속해 있으면 초기 쿼리 1건 + 루프에서 10건 = 총 11번의 DB 조회가 발생합니다. 이는 성능 가이드라인에 위배됩니다.
또한 동일 사용자가 두 개의 탭에서 동시에 같은 이름으로 조직 생성을 요청하면, 두 요청이 모두 중복 체크를 통과해 중복 생성될 가능성이 있습니다. 현재는 메서드 수준의 명시적 락이 없기 때문입니다.
개선 방법:
-
OrgMemberRepository에 다음 메서드를 추가하세요:boolean existsByUserAndOrganization_Name(User user, String orgName);
이렇게 하면 단 1개의 쿼리로 중복 확인이 가능합니다.
-
동시성 문제 방지를 위해 메서드에
@Lock(LockModeType.PESSIMISTIC_WRITE)추가를 검토하거나, DB 레벨에서(user_id, name)복합 유니크 제약을 정의하세요.
🤖 Prompt for AI Agents
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgCRUDService.java`
around lines 41 - 49, The loop in OrgCRUDService that fetches
orgMemberRepository.findOrgMemberByUser(user) and inspects
orgMember.getOrganization().getName() causes N+1 queries and has a race
condition; replace that logic by adding a repository method boolean
existsByUserAndOrganization_Name(User user, String orgName) in
OrgMemberRepository and use
orgMemberRepository.existsByUserAndOrganization_Name(user, request.name()) to
perform a single query for the duplicate check; to prevent concurrent duplicate
creations also either apply a pessimistic lock on the enclosing service method
(e.g., `@Lock`(LockModeType.PESSIMISTIC_WRITE) on the transaction) or enforce a
DB-level unique constraint on (user_id, name).
| public OrgResponse.Read getOrganization(Long userId) { | ||
| //TODO | ||
| return null; | ||
| } | ||
|
|
||
| public void modifyOrganization(Long userId, Long orgId, OrgRequest.Update request) { | ||
| //TODO | ||
|
|
||
| } | ||
|
|
||
| public void removeOrganization(Long userId, Long orgId) { | ||
| //TODO | ||
|
|
||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# 먼저 파일 크기 확인
wc -l src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgCRUDService.javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 171
🏁 Script executed:
# 특정 라인 74-87 확인
sed -n '60,100p' src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgCRUDService.javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 800
🏁 Script executed:
# 전체 파일 구조 확인 (메서드 시그니처)
grep -n "public.*Organization" src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgCRUDService.javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 385
🏁 Script executed:
# 관련 컨트롤러 찾기
find src/main/java/com/whereyouad/WhereYouAd/domains/organization -name "*Controller*" -type fRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 267
🏁 Script executed:
# OrgController 확인
cat -n src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 2895
🏁 Script executed:
# GlobalExceptionHandler 찾기
find src/main/java -name "*GlobalExceptionHandler*" -o -name "*ExceptionHandler*" | grep -i globalRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 157
🏁 Script executed:
# 예외 처리 관련 코드 확인
find src/main/java -path "*/global/*" -o -path "*/common/*" -path "*Exception*" | head -20Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 1499
🏁 Script executed:
# GlobalExceptionHandler 확인
cat -n src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 5191
🏁 Script executed:
# ErrorCode 확인
cat -n src/main/java/com/whereyouad/WhereYouAd/global/exception/ErrorCode.javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 931
🏁 Script executed:
# AppException 확인
cat -n src/main/java/com/whereyouad/WhereYouAd/global/exception/AppException.javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 486
🏁 Script executed:
# 다른 TODO/미구현 메서드 패턴 찾기
grep -r "//TODO\|throw new UnsupportedOperationException\|NOT_IMPLEMENTED" src/main/java --include="*.java" | head -20Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 871
🏁 Script executed:
# 501 상태 코드 혹은 NOT_IMPLEMENTED 패턴이 있는지 확인
grep -r "501\|NOT_IMPLEMENTED\|not.implemented\|미구현" src/main/java --include="*.java"Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 55
🏁 Script executed:
# DataResponse 구조 확인 (null 반환시 어떻게 처리되는지)
cat -n src/main/java/com/whereyouad/WhereYouAd/global/response/DataResponse.javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 1092
미구현 메서드는 명시적으로 실패 처리해 주세요
현재 상태를 확인해보니, 정확히 문제가 있습니다:
getOrganization()는null을 반환하면서ResponseEntity.ok()로 200을 응답 (클라이언트가 성공으로 인식)modifyOrganization()과removeOrganization()은 비어있으면서ResponseEntity.noContent()로 204를 응답 (아무것도 실행되지 않았는데 성공 표시)
이렇게 하면 API 클라이언트가 실제로 처리가 안 됐는데 성공한 줄로 알게 됩니다.
빠른 해결책: UnsupportedOperationException 던지면 됩니다. 다만 현재 설정상 500 에러가 반환되는데, 의미상 더 정확하려면 501 Not Implemented 상태를 쓰는 게 좋습니다.
추천 접근법:
ErrorCodeenum에 501 상태의 NOT_IMPLEMENTED 추가GlobalExceptionHandler에 해당 예외 핸들러 등록- 메서드에서
throw new AppException(ErrorCode.NOT_IMPLEMENTED)던지기
예시:
public OrgResponse.Read getOrganization(Long userId) {
throw new AppException(ErrorCode.NOT_IMPLEMENTED);
}이렇게 하면 클라이언트가 501로 받게 되어 "아직 안 만들어진 기능이구나"를 명확히 알 수 있습니다.
🤖 Prompt for AI Agents
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgCRUDService.java`
around lines 74 - 87, The three unimplemented methods getOrganization(Long
userId), modifyOrganization(Long userId, Long orgId, OrgRequest.Update request),
and removeOrganization(Long userId, Long orgId) currently return null/are empty
and cause misleading 200/204 responses; update them to throw a clear
not-implemented exception by throwing new
AppException(ErrorCode.NOT_IMPLEMENTED). If ErrorCode lacks NOT_IMPLEMENTED, add
NOT_IMPLEMENTED with HTTP 501 semantics to the ErrorCode enum and ensure
GlobalExceptionHandler maps AppException(ErrorCode.NOT_IMPLEMENTED) to an HTTP
501 response so clients receive a proper 501 Not Implemented for these methods.
| @ApiResponses({ | ||
| @ApiResponse(responseCode = "200", description = "성공"), | ||
| @ApiResponse(responseCode = "400_1", description = "조직 이름 중복") | ||
| }) | ||
| public ResponseEntity<DataResponse<OrgResponse.Create>> createOrganization(@AuthenticationPrincipal(expression = "userId") Long userId, | ||
| @RequestBody @Valid OrgRequest.Create request); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# 파일 위치 확인 및 전체 내용 보기
find . -name "OrgControllerDocs.java" -type fRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 176
🏁 Script executed:
# 해당 파일의 전체 내용 확인
cat -n src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 1563
🏁 Script executed:
# 실제 Controller 구현 파일 찾기
find . -name "*OrgController.java" -type f | grep -v DocsRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 167
🏁 Script executed:
# 프로젝트 내 다른 `@ApiResponse` 패턴 찾기 (responseCode 형식 확인)
rg "@ApiResponse.*responseCode" --type java -A 1 | head -50Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 5797
🏁 Script executed:
# 다른 인증 필요 엔드포인트 확인
rg "@AuthenticationPrincipal" --type java -B 5 | grep -A 5 "ApiResponse" | head -60Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 3827
🏁 Script executed:
# 프로젝트 전체 ApiResponse 형식 통계 (400_X 패턴이 정말 광범위한지 확인)
rg "responseCode.*=.*\"[0-9]" --type java | wc -lRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 75
🏁 Script executed:
# 표준 HTTP 상태코드만 사용하는 경우가 있는지 확인
rg "responseCode.*=.*\"[0-9]{3}\"" --type java | head -20Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 3197
🏁 Script executed:
# OrgController 구현 확인 (실제 에러 처리 방식)
cat -n src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java | head -60Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 2873
OpenAPI 스펙 준수: responseCode는 HTTP 상태 코드만 사용해야 합니다.
responseCode = "400_1"는 OpenAPI 3.0 스펙 위반입니다. Swagger UI 렌더링이나 클라이언트 코드 생성 도구에서 문제가 발생할 수 있어요. 현재 프로젝트 전역에서 이 패턴(400_1, 401_1 등)을 사용 중인데, 이를 기회로 체계적으로 정규 HTTP 상태 코드(400, 401, 404 등)로 통일하고 세부 에러 코드는 response body에 담는 구조로 리팩토링하길 권장합니다.
또한 이 엔드포인트는 @AuthenticationPrincipal로 인증이 필수인데, 인증 실패 시나리오(401)가 문서화되지 않았습니다. 다른 인증 필요 엔드포인트들도 마찬가지로 401을 빠뜨리고 있으니 함께 보완해주세요.
🛠️ 예시 변경 (프로젝트 전역 통일 권장)
`@ApiResponses`({
`@ApiResponse`(responseCode = "200", description = "성공"),
- `@ApiResponse`(responseCode = "400_1", description = "조직 이름 중복")
+ `@ApiResponse`(responseCode = "400", description = "조직 이름 중복 (ORG_400_1)"),
+ `@ApiResponse`(responseCode = "401", description = "인증 필요")
})🤖 Prompt for AI Agents
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java`
around lines 19 - 24, The `@ApiResponses` on OrgControllerDocs.createOrganization
uses a non-standard responseCode "400_1"; change it to a valid HTTP status code
(e.g., "400") and add a "401" ApiResponse to document authentication failure;
keep any fine-grained error identifiers in the response body (not in
responseCode). Update the `@ApiResponse` entries for createOrganization (and other
controllers that follow the same pattern) to use standard numeric HTTP status
strings and ensure the response schema/body contains the project-specific error
code/details instead of using "400_1" style codes.
kingmingyu
left a comment
There was a problem hiding this comment.
고생하셨습니다! 항상 되게 빠르시네요! 저도 동일한 사람이 동일한 조직을 만드는건 불필요하다고 생각해서 예외처리도 괜찮은 것 같습니다!
src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java
Show resolved
Hide resolved
src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/handler/OrgHandler.java
Show resolved
Hide resolved
src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgCRUDService.java
Outdated
Show resolved
Hide resolved
jinnieusLab
left a comment
There was a problem hiding this comment.
P4: 고생하셨습니다! 조직 생성한 유저가 ADMIN 권한을 갖도록 로직이 잘 구현되어 있는 듯 해 좋습니다!
src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java
Show resolved
Hide resolved
src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java
Show resolved
Hide resolved
|
Swagger @hidden 어노테이션으로 미완성 API 숨김처리 하였고, Builder 패턴 Converter 로 리팩터링 진행하는 거랑 |
📌 관련 이슈
🚀 개요
조직(워크스페이스) 생성 API 추가 및 조회, 수정, 삭제 에 대한 스켈레톤 코드 추가
📄 작업 내용
📸 스크린샷 / 테스트 결과 (선택)
==조직 정상 생성 로직==
===예외 처리===

DB 에 test 유저가 org1 이라는 이름의 조직을 만든 상태에서, 똑같은 이름(org1)의 조직 생성 시도 시 오류
AccessToken 없이 접근 시 오류

✅ 체크리스트
🔍 리뷰 포인트 (Review Points)
Summary by CodeRabbit