Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
58c482f
:sparkles: feat: 조직 조회를 위한 Response DTO, Converter 코드 추가
ojy0903 Feb 17, 2026
67fc0c1
:sparkles: feat: 조직 조회를 위한 OrgRepository, OrgMemberRepository 커스텀 조회 …
ojy0903 Feb 17, 2026
ede17d3
:sparkles: feat: 조직 조회 시 삭제된 조직에 대한 접근 방지용 OrgErrorCode 오류 코드 추가
ojy0903 Feb 17, 2026
d4bb469
:sparkles: feat: 조직 조회 시 Service 로직 추가 (로그인한 회원이 속한 조직 리스트 조회, 특정 조직 …
ojy0903 Feb 17, 2026
a365d32
:sparkles: feat: 조직 조회 시 API 추가 (로그인한 회원이 속한 조직 리스트 조회, 특정 조직 id 값을 받…
ojy0903 Feb 17, 2026
1f666db
:sparkles: feat: RequestParam 에 param 정보 누락 시 오류 handling 코드 GlobalEx…
ojy0903 Feb 17, 2026
befe198
:art: style: 미사용 import 문 제거
ojy0903 Feb 17, 2026
4e2ddd9
:bug: fix: 조직 조회 DTO 내부 회원정보 관련 데이터 제거, 페이징 로직 제거
ojy0903 Feb 18, 2026
f28a530
:bug: fix: OrgMemberRepository 내부 페이징 관련 메서드 -> 단순 List 반환으로 수정
ojy0903 Feb 18, 2026
54b0483
:bug: fix: Service 코드 내부 "이름으로 조직 조회 메서드" 제거, "내 조직 조회" 메서드 페이징 제거, "…
ojy0903 Feb 18, 2026
23a87a3
:bug: fix: Service 조직 조회 API 수정
ojy0903 Feb 18, 2026
2005842
:bug: fix: 미사용 import 제거
ojy0903 Feb 18, 2026
f031b8e
:bug: fix: 미사용 import 제거
ojy0903 Feb 18, 2026
752821a
Merge remote-tracking branch 'origin/develop' into feat/#33
ojy0903 Feb 18, 2026
9e67466
:bug: fix: conflict 조정 이후 미사용 import 제거
ojy0903 Feb 18, 2026
d8e9c40
:art: style: 주석 수정
ojy0903 Feb 18, 2026
63db05c
:recycle: refactor: 내가 속한 조직 조회 시 coderabbit 에 따라 OrgMember 내부 role 값…
ojy0903 Feb 18, 2026
731cfdc
:recycle: refactor: OrgRepository 내부 미사용 메서드 제거
ojy0903 Feb 18, 2026
99b20cb
:sparkles: feat: OrgController Swagger Docs 내부 오류 응답 형식 추가
ojy0903 Feb 18, 2026
f7ec6a2
:art: style: 미사용 import 문 제거
ojy0903 Feb 18, 2026
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
@@ -1,5 +1,7 @@
package com.whereyouad.WhereYouAd.domains.organization.application.dto.response;

import com.whereyouad.WhereYouAd.domains.organization.domain.constant.OrgRole;

import java.time.LocalDateTime;
import java.util.List;

Expand All @@ -10,8 +12,28 @@ public record Create(
LocalDateTime createdAt
) {}

public record Read (
//TODO
//내 조직 정보 조회를 위한 간략화된 조직 정보 DTO
public record SimpleInfo(
Long orgId,
String name,
String description,
String logoUrl,
OrgRole myRole
) {}

//내 조직 정보는 SimpleInfo 를 List 로 반환
//MyOrganization DTO 내부에 SimpleInfo DTO 가 여러개 포함
public record MyOrganizations(
List<SimpleInfo> organizations
) {}

//하나의 조직 세부 정보 반환 DTO
public record OrgDetail (
Long orgId,
String name,
String description,
String logoUrl,
LocalDateTime createdAt
) {}

public record Update (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.whereyouad.WhereYouAd.domains.organization.application.dto.request.OrgRequest;
import com.whereyouad.WhereYouAd.domains.organization.application.dto.response.OrgResponse;
import com.whereyouad.WhereYouAd.domains.organization.domain.constant.OrgRole;
import com.whereyouad.WhereYouAd.domains.organization.domain.constant.OrgStatus;
import com.whereyouad.WhereYouAd.domains.organization.persistence.entity.OrgMember;
import com.whereyouad.WhereYouAd.domains.organization.persistence.entity.Organization;
Expand All @@ -28,6 +29,37 @@ public static OrgResponse.Delete toRestoredResponse(Organization organization) {
"해당 조직이 활성화 되었습니다");
}

public static OrgResponse.MyOrganizations toMyOrganizations(List<OrgResponse.SimpleInfo> infos) {
return new OrgResponse.MyOrganizations(
infos
);
}

public static OrgResponse.OrgDetail toOrgDetail(Organization organization) {
return new OrgResponse.OrgDetail(organization.getId(),
organization.getName(),
organization.getDescription(),
organization.getLogoUrl(),
organization.getCreatedAt()
);
}

public static OrgResponse.SimpleInfo toOrgSimpleInfo(Organization organization, Long userId) {
OrgRole myRole;

if (organization.getOwnerUserId().equals(userId)) {
myRole = OrgRole.ADMIN;
} else {
myRole = OrgRole.MEMBER;
}

return new OrgResponse.SimpleInfo(organization.getId(),
organization.getName(),
organization.getDescription(),
organization.getLogoUrl(),
myRole);
}

//DTO -> Entity
public static Organization toOrganization(Long userId, OrgRequest.Create request) {
return Organization.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ public interface OrgService {

OrgResponse.Create createOrganization(Long userId, OrgRequest.Create request);

OrgResponse.Read getOrganization(Long userId);
OrgResponse.MyOrganizations getMyOrganizations(Long userId);

OrgResponse.OrgDetail getOrganizationDetail(Long orgId);

OrgResponse.Update modifyOrganization(Long userId, Long orgId, OrgRequest.Update request);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

@Service
Expand Down Expand Up @@ -59,9 +60,43 @@ public OrgResponse.Create createOrganization(Long userId, OrgRequest.Create requ
return OrgConverter.toCreatedResponse(organization);
}

public OrgResponse.Read getOrganization(Long userId) {
//TODO
return null;
//로그인한 회원이 속한 조직 모두 조회 메서드
public OrgResponse.MyOrganizations getMyOrganizations(Long userId) {
//회원 id 로 OrgMember 모두 조회 -> DB 조회에서 OrgStatus.ACTIVE 인 Organization 만 포함하는 OrgMember 만 조회해 온다.
List<OrgMember> orgMembers = orgMemberRepository.findOrgMemberByUserId(userId);

//각 OrgMember 에 Organization 추출
List<Organization> organizations = new ArrayList<>();

for (OrgMember orgMember : orgMembers) {
organizations.add(orgMember.getOrganization());
}

//각각의 Organization Entity 를 DTO 로 변환
List<OrgResponse.SimpleInfo> infos = new ArrayList<>();

for (Organization organization : organizations) {
OrgResponse.SimpleInfo info = OrgConverter.toOrgSimpleInfo(organization, userId);
infos.add(info);
}

//마지막 반환 DTO 로 변환
return OrgConverter.toMyOrganizations(infos);
}

//하나의 조직에 대한 세부 사항(ID, 이름, 설명, logoUrl, createdAt)
public OrgResponse.OrgDetail getOrganizationDetail(Long orgId) {
//해당 조직 id 로 Organization 조회
Organization organization = orgRepository.findById(orgId)
.orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND));

//Soft Delete 된 조직이면 예외처리
if (organization.getStatus() == OrgStatus.DELETED) {
throw new OrgHandler(OrgErrorCode.ORG_SOFT_DELETED);
}

//DTO 로 변환
return OrgConverter.toOrgDetail(organization);
}
Comment on lines +77 to 89
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

조직 비공개 여부에 따라 멤버십 접근 제어 검토 필요

현재 구현은 삭제된 조직 여부(소프트 딜리트)만 확인하고, 요청한 사용자가 해당 조직의 멤버인지는 검증하지 않습니다. 즉, 인증된 사용자라면 조직 id만 알면 본인이 속하지 않은 조직의 상세 정보도 조회할 수 있습니다.

비공개 조직이나 B2B 테넌트 격리가 요구사항에 있다면 아래와 같은 멤버십 검증 로직을 추가하는 것을 권장합니다:

// 예시: orgMemberRepository를 통해 멤버십 확인
boolean isMember = orgMemberRepository.existsByOrganizationIdAndUserId(orgId, currentUserId);
if (!isMember) {
    throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN);
}

요구사항상 조직 상세 정보가 인증된 사용자 누구에게나 공개되는 것이라면 현재 구현도 문제없습니다. 의도된 설계인지 확인 부탁드립니다.

🤖 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/organization/domain/service/OrgServiceImpl.java`
around lines 77 - 89, The getOrganizationDetail method currently only checks
soft-deletion; add membership/access control: obtain the current user id (via
your auth component, e.g., SecurityContextHolder or an injected AuthService
method such as getCurrentUserId()), then use
orgMemberRepository.existsByOrganizationIdAndUserId(orgId, currentUserId) (or
equivalent) to verify the requester is a member (or otherwise allowed) and if
not throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN); keep the existing
orgRepository lookup and DELETED check and perform the membership check before
converting to DTO in OrgConverter.toOrgDetail.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find OrgService interface and OrgServiceImpl, then examine structure

echo "=== Finding OrgService interface (excluding Impl) ==="
fd -e java "OrgService\.java" 

echo ""
echo "=== Finding OrgServiceImpl ==="
fd -e java "OrgServiceImpl\.java"

echo ""
echo "=== Finding OrgController ==="
fd -e java "OrgController\.java"

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 470


🏁 Script executed:

#!/bin/bash
# Examine the three key files

echo "=== OrgService.java (interface) ==="
cat -n src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java

echo ""
echo "=== OrgServiceImpl.java (class-level annotations + methods) ==="
head -100 src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java

echo ""
echo "=== OrgController.java (injection check) ==="
head -50 src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 7754


🏁 Script executed:

#!/bin/bash
# Verify the exact class-level `@Transactional` annotation in OrgServiceImpl

echo "=== Class-level `@Transactional` in OrgServiceImpl ==="
sed -n '20,28p' src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java

echo ""
echo "=== Check getOrganizationDetail method signature (lines 75-90) ==="
sed -n '75,90p' src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 945


순수 조회 메서드에 @Transactional(readOnly = true) 추가 필요

이 메서드는 Organization을 조회하고 상태를 확인한 후 DTO로 변환하는 순수 읽기 작업입니다. 현재는 클래스 레벨의 @Transactional(기본값: readOnly = false)에서 실행되고 있는데, 이는 쓰기 작업을 위한 설정입니다.

readOnly = true를 추가하면:

  • 데이터베이스 레벨에서 읽기 전용 트랜잭션으로 최적화됨
  • Dirty checking 오버헤드 제거 (변경사항 감지 안 함)
  • 명확한 의도 전달 (이 메서드는 조회만 한다)
♻️ 개선 제안
+    `@Transactional`(readOnly = true)
     public OrgResponse.OrgDetail getOrganizationDetail(Long orgId) {

현재 코드는 OrgService 인터페이스에 메서드가 잘 선언되어 있고(12번 줄), OrgController도 인터페이스 타입으로 주입받기 때문에 정상 작동합니다. 좋은 DI 패턴이네요. 👍

🤖 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/organization/domain/service/OrgServiceImpl.java`
around lines 77 - 89, getOrganizationDetail in OrgServiceImpl is a read-only
lookup but runs under the class-level `@Transactional` (readOnly=false); annotate
the method getOrganizationDetail(...) with `@Transactional`(readOnly = true) to
enforce a read-only transaction, preventing unnecessary dirty-checking and
signaling intent, leaving the existing logic that uses
orgRepository.findById(...) and OrgConverter.toOrgDetail(...) unchanged.


//조직 정보 수정 메서드
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ public enum OrgErrorCode implements BaseErrorCode {
ORG_NOT_FOUND(HttpStatus.NOT_FOUND, "ORG_404_1", "해당 id 의 조직이 존재하지 않습니다."),

//409
ORG_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_409_1", "해당 조직은 이미 활성화 상태 입니다.")
ORG_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_409_1", "해당 조직은 이미 활성화 상태 입니다."),


//410
ORG_SOFT_DELETED(HttpStatus.GONE, "ORG_410_1", "해당 조직은 삭제된 조직입니다.(Soft Delete)"),
Comment on lines +24 to +25
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

410 Gone 사용은 의미론적으로 정확합니다! 👍 메시지 문자열에 공백 하나가 빠졌습니다.

소프트 딜리트된 리소스에 HttpStatus.GONE을 사용한 건 HTTP 스펙에 맞는 좋은 선택입니다. 다만 현재 메시지 "해당 조직은 삭제된 조직입니다.(Soft Delete)"(Soft Delete) 앞에 공백이 없어서 PR 스크린샷에 보이는 응답 예시("해당 조직은 삭제된 조직입니다. (Soft Delete)")와 다릅니다.

✏️ 메시지 수정 제안
-ORG_SOFT_DELETED(HttpStatus.GONE, "ORG_410_1", "해당 조직은 삭제된 조직입니다.(Soft Delete)"),
+ORG_SOFT_DELETED(HttpStatus.GONE, "ORG_410_1", "해당 조직은 삭제된 조직입니다. (Soft Delete)"),
📝 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
//410
ORG_SOFT_DELETED(HttpStatus.GONE, "ORG_410_1", "해당 조직은 삭제된 조직입니다.(Soft Delete)"),
//410
ORG_SOFT_DELETED(HttpStatus.GONE, "ORG_410_1", "해당 조직은 삭제된 조직입니다. (Soft Delete)"),
🤖 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/organization/exception/code/OrgErrorCode.java`
around lines 24 - 25, The enum constant ORG_SOFT_DELETED has a missing space
before the parenthetical; update its message string from "해당 조직은 삭제된 조직입니다.(Soft
Delete)" to include a space before "(Soft Delete)" so it reads "해당 조직은 삭제된
조직입니다. (Soft Delete)" in the ORG_SOFT_DELETED definition.

;
private final HttpStatus httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ public interface OrgMemberRepository extends JpaRepository<OrgMember, Long> {
//User 가 가진 OrgMember 모두 추출하는 메서드
List<OrgMember> findOrgMemberByUser(User user);

//userId 를 통해 OrgMember 추출 -> Organization 의 status 가 ACTIVE 인 경우에만 조회
@Query(value = "select om from OrgMember om join fetch om.organization o where om.user.id = :userId and o.status = 'ACTIVE'")
List<OrgMember> findOrgMemberByUserId(@Param("userId") Long userId);

//특정 Organization 에 속한 OrgMember 모두 추출하는 메서드
@Query("select om from OrgMember om where om.organization = :organization")
List<OrgMember> findOrgMemberByOrg(@Param(value = "organization") Organization organization);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
package com.whereyouad.WhereYouAd.domains.organization.persistence.repository;

import com.whereyouad.WhereYouAd.domains.organization.persistence.entity.Organization;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface OrgRepository extends JpaRepository<Organization, Long> {

//검색하려는 문자열이 포함된 name 을 가진 조직을 추출
@Query("select o from Organization o where o.name like concat('%', :name, '%') AND o.status = 'ACTIVE'")
Page<Organization> findOrganizationsByName(@Param(value = "name") String name, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import com.whereyouad.WhereYouAd.domains.organization.domain.service.OrgService;
import com.whereyouad.WhereYouAd.domains.organization.presentation.docs.OrgControllerDocs;
import com.whereyouad.WhereYouAd.global.response.DataResponse;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
Expand All @@ -32,12 +31,23 @@ public ResponseEntity<DataResponse<OrgResponse.Create>> createOrganization(
);
}

@Hidden
@GetMapping("/read")
public ResponseEntity<DataResponse<OrgResponse.Read>> getOrganizations(
@GetMapping("/my")
public ResponseEntity<DataResponse<OrgResponse.MyOrganizations>> getMyOrganizations(
@AuthenticationPrincipal(expression = "userId") Long userId
) {
OrgResponse.Read response = orgService.getOrganization(userId);
)
{
OrgResponse.MyOrganizations response = orgService.getMyOrganizations(userId);

return ResponseEntity.ok(
DataResponse.from(response)
);
}

@GetMapping("/{orgId}")
public ResponseEntity<DataResponse<OrgResponse.OrgDetail>> getOrganizationDetail(@PathVariable Long orgId)
{
OrgResponse.OrgDetail response = orgService.getOrganizationDetail(orgId);

return ResponseEntity.ok(
DataResponse.from(response)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,28 @@ public interface OrgControllerDocs {
public ResponseEntity<DataResponse<OrgResponse.Create>> createOrganization(@AuthenticationPrincipal(expression = "userId") Long userId,
@RequestBody @Valid OrgRequest.Create request);

@Operation(
summary = "내가 속한 조직 전체 조회 API",
description = "로그인한 회원이 속한 조직들의 DB id, 이름, 설명, 로고URL, 내 역할(ADMIN/MEMBER) 을 반환"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공"),
@ApiResponse(responseCode = "401_3", description = "토큰 없이 접근 시 실패")
})
public ResponseEntity<DataResponse<OrgResponse.MyOrganizations>> getMyOrganizations(
@AuthenticationPrincipal(expression = "userId") Long userId);

@Operation(
summary = "조직 하나의 세부정보 조회 API",
description = "조직 id 를 param 으로 받아 해당 조직의 id, 이름, 설명, 로고URL, 조직 생성 시각 반환"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공"),
@ApiResponse(responseCode = "404_1", description = "해당 id 값 조직 존재 X")
})
public ResponseEntity<DataResponse<OrgResponse.OrgDetail>> getOrganizationDetail(@PathVariable Long orgId);


@Operation(
summary = "조직 정보 수정 API",
description = "새로운 조직 이름, 설명, 로고 이미지 URL 을 받아 저장(해당 조직을 생성한 회원만 정보 변경 가능)"
Expand Down