본문 바로가기
Projects/청하-청년을 위한 커뮤니티 서비스

[청하] 27. 정책 상세 조회 기능 구현

by Lpromotion 2024. 10. 18.

“정책 리스트 조회”에 이어 “정책 상세 조회” 기능을 구현했다.

“정책 상세 조회”에서는 각 정책의 세부 내용을 확인할 수 있다.

 

1. 프로젝트 구조

src/main/java/com/example/withpeace/
│
├── domain/                 # 도메인 모델 (엔티티)
│   └── YouthPolicy.java    # 청년 정책 엔티티
│
├── repository/                       # 데이터 접근 계층
│   └── YouthPolicyRepository.java    # 게시글 레포지토리
│
├── dto/                                     # 데이터 전송 객체
│   └── response/
│       ├── PolicyListResponseDto.java       # 정책 리스트 응답 DTO
│       └── PolicyDetailResponseDto.java     # 정책 상세조회 응답 DTO
│
├── controller/               # 컨트롤러 계층
│   └── PolicyController.java # 정책 관련 API 엔드포인트
│
└── service/                  # 비즈니스 로직 계층
    └── PolicyService.java    # 정책 관련 비즈니스 로직

 

2. 도메인 엔티티 구현

YouthPolicy

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DynamicUpdate
@Table(name = "youth_policies")
public class YouthPolicy {
    // ... (기존 필드 생략)
    
    @Column(name = "etc")
    private String etc; // 기타 유익 정보

    @Column(name = "managing_institution")
    private String managingInstitution; // 주관 기관

    @Column(name = "operating_organization")
    private String operatingOrganization; // 운영 기관

    @Column(name = "business_reference_site1")
    private String businessReferenceSite1; // 사업관련 참고 사이트1

    @Column(name = "business_reference_site2")
    private String businessReferenceSite2; // 사업관련 참고 사이트2

    @Builder
    public YouthPolicy(String rnum, String id, String title, String introduce, String regionCode, String classificationCode,
                       String ageInfo, String applicationDetails, String residenceAndIncome, String education,
                       String specialization, String additionalNotes, String participationRestrictions,
                       String applicationProcess, String screeningAndAnnouncement, String applicationSite,
                       String submissionDocuments, String etc, String managingInstitution, String operatingOrganization,
                       String businessReferenceSite1, String businessReferenceSite2) {
        this.rnum = Long.parseLong(rnum);
        this.id = id;
        this.title = title;
        this.introduce = introduce == null || introduce.equals("null") ? "-" : introduce;
        this.region = EPolicyRegion.fromCode(regionCode);
        this.classification = EPolicyClassification.fromCode(classificationCode);
        this.ageInfo = ageInfo == null || ageInfo.equals("null") ? "-" : ageInfo;
        this.applicationDetails = applicationDetails == null || applicationDetails.equals("null") ? "-" : applicationDetails;
        // ... (다른 필드들에 대해서도 동일한 null 처리)
    }
}

정책 리스트 조회 API 보다 더 상세한 정보를 조회할 수 있도록 etc, managingInstitution, operatingOrganization, businessReferenceSite1, businessReferenceSite2 필드를 추가했다.

 

null 값 처리 로직

// 기존
this.ageInfo = ageInfo.equals("null") ? "-" : ageInfo;

// 변경
this.ageInfo = ageInfo == null || ageInfo.equals("null") ? "-" : ageInfo;

이전 게시글와 비교하여 달라진 점이 있다. 정책 API 연동 작업을 할 때까지만 해도 받아오는 데이터에서 문자열 “null”만 처리해주면 성공적으로 잘 저장되었는데, 최근에 등록된 데이터 중에 실제 null 값이 들어가는 경우가 생긴 것이다. 그래서 문자열 “null”과 실제 null 값을 모두 처리하도록 변경했다.

 

3. 레포지토리 구현

YouthPolicyRepository

public interface YouthPolicyRepository extends JpaRepository<YouthPolicy, Long> {
    // ... (기존 코드 생략)

    Optional<YouthPolicy> findById(String policyId);
}

`findById` 는 정책 ID를 통해 해당하는 정책을 조회한다.

`Optional<YouthPolicy>` 로 반환하여 정책이 존재하지 않는 경우를 처리할 수 있다. (NullPointerException 방지)

 

4. DTO 구현

4.1. YouthPolicyResponseDto 수정

@JsonIgnoreProperties(ignoreUnknown = true)
public record YouthPolicyResponseDto(
        // ... (기존 생략)

        // 추가
        @JacksonXmlProperty(localName = "etct")
        String etc,

        @JacksonXmlProperty(localName = "mngtMson")
        String managingInstitution,

        @JacksonXmlProperty(localName = "cnsgNmor")
        String operatingOrganization,

        @JacksonXmlProperty(localName = "rfcSiteUrla1")
        String businessReferenceSite1,

        @JacksonXmlProperty(localName = "rfcSiteUrla2")
        String businessReferenceSite2
) {
}

청년 정책 Open API의 응답을 매핑하기 위한 DTO 클래스이다.

상세 조회를 위한 추가적인 데이터를 수집하기 위해 필드를 추가했다.

`@JsonIgnoreProperties(ignoreUnknown = true)` 를 사용해 DTO에 정의되지 않은 필드가 API에 포함되어도 오류가 발생하지 않는다.

 

4.2. PolicyDetailResponseDto

@Builder
public record PolicyDetailResponseDto(
        String id,
        String title,
        String introduce,
        EPolicyClassification classification,
        String applicationDetails,

        String ageInfo,
        String residenceAndIncome,
        String education,
        String specialization,
        String additionalNotes,
        String participationRestrictions,

        String applicationProcess,
        String screeningAndAnnouncement,
        String applicationSite,
        String submissionDocuments,

        String additionalUsefulInformation, // etc
        String supervisingAuthority, // managingInstitution
        String operatingOrganization,
        String businessRelatedReferenceSite1, // businessReferenceSite1
        String businessRelatedReferenceSite2 // businessReferenceSite2
) {
    public static PolicyDetailResponseDto from(YouthPolicy policy) {
        return PolicyDetailResponseDto.builder()
                .id(policy.getId())
                .title(policy.getTitle())
                .introduce(policy.getIntroduce())
                // ... (다른 필드 매핑)
                .build();
    }
}

정책 상세 조회 API 호출에 반환하기 위한 DTO 이다. YouthPolicy 엔티티에서 필요한 정보를 추출해 제공하고, 필드들은 해당 정책의 상세 정보를 담고 있다.

 

from 메서드

YouthPolicy 엔티티를 PolicyDetailResponseDto 로 변환하는 정적 팩토리 메서드이다.

서비스 계층에서 YouthPolicy 객체를 직접적으로 DTO 객체로 변환하는 작업을 수행해도 되지만, 코드의 가독성을 높이고 유지보수를 용이하게 하기 위해 from 메서드를 사용하는 방법으로 구현했다.

 

정적 팩토리 메서드

[청하] 10. 게시글 리스트 조회 기능 구현 에서도 작성했지만 추가로 덧붙이자면

from 메서드는 OOP(객체 지향 프로그래밍)의 개념 중 Factory Method 해당한다. Factory Method 는 디자인 패턴 중 하나로 객체를 생성하는 메서드를 따로 정의하여 객체 생성 로직을 캡슐화하는 패턴이다.

static 으로 정의한 이유는 from 메서드가 객체를 변환하는 *유틸리티성 메서드이므로, 객체를 생성하고 변환하는 것이 목적이다. 따라서 객체의 인스턴스가 아닌 클래스 수준에서 정적 메서드로 제공한다.

* 유틸리티성: 프로그램에서 재사용 가능한 일반적인 기능을 제공하며, 코드의 효율성과 재사용성을 높이는 개념

 

5. 컨트롤러 구현

YouthPolicyController

@RestController
@RequiredArgsConstructor
@RequestMapping("api/v1/policies")
@Slf4j
public class YouthPolicyController {
    private final YouthPolicyService youthPolicyService;
    // ... (기존 코드 생략)

    // 정책 상세 조회
    @GetMapping("/{policyId}")
    public ResponseDto<?> getPolicyDetail(@PathVariable String policyId) {
        return ResponseDto.ok(youthPolicyService.getPolicyDetail(policyId));
    }
}

정책 ID를 입력받아 서비스로 전달한다.

`@PathVariable` 을 사용해 URL 경로에서 정책 ID를 받아온다.

 

6. 서비스 구현

YouthPolicyService

@Service
@RequiredArgsConstructor
@Slf4j
public class YouthPolicyService {
    private final YouthPolicyRepository youthPolicyRepository;

    // 정챡 ID로 정책 조회
    private YouthPolicy getPolicyById(String policyId) {
        return youthPolicyRepository.findById(policyId).orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_YOUTH_POLICY));
    }

    // ... (기존 코드 생략)

    // 정책 상제 정보 조회
    @Transactional
    public PolicyDetailResponseDto getPolicyDetail(String policyId) {
        YouthPolicy policy = getPolicyById(policyId);
        return PolicyDetailResponseDto.from(policy);
    }

}

 

6.1. getPolicyById 메서드

정책 ID를 받아 해당 정책을 데이터베이스에서 조회한다. 해당하는 정책이 없는 경우 CommonException를 발생시킨다.

 

6.2. getPolicyDetail 메서드

정책 ID를 입력받아 정책 상세 정보를 조회한다.

`getPolicyById(policyId)`를 호출해 정책 객체를 가져오고, 이 객체를 `PolicyDetailResponseDto.from(policy)` 를 통해 DTO 객체로 변환해 반환한다.

 

7. API 응답 예시

요청 URL

[GET] http://cheongha.site/api/v1/policies/R2024070424704

 

Response Body

{
    "data": {
        "id": "R2024070424704",
        "title": "청년 로컬디저트 메뉴개발 컨설팅",
        "introduce": "청년창업가의 창업 경쟁력을 높이기 위해 로컬을 활용한 디저트 메뉴개발을 지원",
        "classification": "JOB",
        "applicationDetails": "□ 로컬 디저트 메뉴 개발(시크니처 메뉴)지원\\n- 메뉴개발컨설팅, 실습공간·식재료지원, 시식회, 정책연계 등\\n□ 교육기간 : 2024. 7월 ~ 10월(4개월)\\n□ 교육장소 : 청년 로컬크리에이터 라운지(북구 행복어울림센터 1층)\\n※ 북구 행복어울림센터 : 북구 용봉로 105(용봉동), 전남대 정문에서 북구청 사이\\n□ 지원분야\\n❍ 로컬일반 : 로컬재료활용, 지역문화·디자인 접목 등\\n❍ 북구특성화 : 북구도시브랜드(부끄부부)활용\\n□ 후속지원\\n❍ 시제품 매장 판매실습\\n❍ 창업 지원정책 연계",
        "ageInfo": "만 19세 ~ 39세",
        "residenceAndIncome": "북구 관내 사업장 소재 청년 초기창업자(5년이내), 예비창업자",
        "education": "-",
        "specialization": "-",
        "additionalNotes": "-",
        "participationRestrictions": "-",
        "applicationProcess": "접수방법 : 청년센터 홈페이지 온라인신청<공지사항>",
        "screeningAndAnnouncement": "최종선정결과 발표 : 7월12일(금)",
        "applicationSite": "<https://bukgu.gwangju.kr/youth/>",
        "submissionDocuments": "참여신청서, 사업계획서, 사업자등록증, 창업공간 사진 등",
        "additionalUsefulInformation": "-",
        "supervisingAuthority": "-",
        "operatingOrganization": "-",
        "businessRelatedReferenceSite1": "-",
        "businessRelatedReferenceSite2": "-"
    },
    "error": null
}
반응형

댓글