> For the complete documentation index, see [llms.txt](https://tech.x2bee.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tech.x2bee.com/dev-guide/dev-start/integrations/undefined-2.md).

# 서버 측 개발 방법

이 문서는 서버 측 개발 방식에 대해서 다룹니다.\
서버에서의 개발 패턴과 디렉토리 구조를 설명하고, 각 클래스 작성 방법에 대해 설명합니다.

***

## Controller, Service, (APIController, service), Mapper 구조

서버는 Spring MVC 패턴 기반으로 구성합니다.\
데이터 처리를 위하여 Controller → Service → Mapper 구조로 진행합니다.\
MAS 구조로 개발되며 Front(Next.js)에서 API서버를 호출하여 DB서버와 통신하는 형태로 개발합니다.

아래는 프로그램 호출 순서입니다

http request → (mapping) → Controller → Service → ServiceImpl → APIController → Service → ServiceImpl → Mapper → XML(query) (BO서버) (API서버)

***

**디렉토리 구조**

<figure><img src="/files/s94Gb1hIdptDycc60Sd8" alt=""><figcaption></figcaption></figure>

***

## DTO 클래스 작성

* src/main/java 하위 해당 업무 폴더에 dto, entity 폴더 생성 후 작업합니다.
* 데이터 전달을 위하여 사용될 객체를 사전 정의합니다.

**Sample.java**

{% code title="Sample.java" %}

```java
package com.x2bee.api.bo.app.entity;

import javax.validation.constraints.NotNull;
import org.apache.ibatis.type.Alias;
import com.x2bee.common.base.entity.BaseCommonEntity;
import lombok.Getter;
import lombok.Setter;

@Alias("Sample")
@Getter
@Setter
public class Sample extends BaseCommonEntity {
    private static final long serialVersionUID = -5756700830219562201L;
    private Long id;
    @NotNull
    private String name;
    private String description;
}
```

{% endcode %}

***

## API Controller 클래스 작성

* API 서버의 Controller 작성
* Response 객체로 Return

**SampleController.java**

{% code title="SampleController.java" %}

```java
package com.x2bee.api.bo.app.controller.sample;

import java.util.List;
import javax.validation.Valid;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.x2bee.api.bo.app.dto.request.common.SampleMpicRequest;
import com.x2bee.api.bo.app.dto.request.sample.SampleRequest;
import com.x2bee.api.bo.app.dto.response.sample.SampleResponse;
import com.x2bee.api.bo.app.entity.Sample;
import com.x2bee.api.bo.app.service.sample.SampleService;
import com.x2bee.api.bo.base.advice.ApiError;
import com.x2bee.api.bo.base.annotation.IndInfoLog;
import com.x2bee.common.base.exception.AppException;
import com.x2bee.common.base.rest.Response;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@RestController
@RequestMapping("/samples")
@Lazy
@Slf4j
@RequiredArgsConstructor
public class SampleController {

    private final SampleService sampleService;

    @GetMapping("")
    public Response<List<SampleResponse>> getAllSamples() {
        return new Response<List<SampleResponse>>().setPayload(sampleService.getAllSamples());
    }

    @GetMapping("/{id}")
    public Response<SampleResponse> getSample(@PathVariable Long id,
            @RequestHeader(value="test-header-key1", required = false) String testHeader1) {
        log.info("id: {}, testHeader1: {}", id, testHeader1);
        return new Response<SampleResponse>().setPayload(sampleService.getSample(id));
    }

    @GetMapping("/search")
    public Response<List<SampleResponse>> searchSamples(SampleRequest sampleRequest) {
        log.info("sampleRequest: {}", sampleRequest);
        return new Response<List<SampleResponse>>().setPayload(sampleService.searchSamples(sampleRequest));
    }

    @PostMapping("")
    public Response<String> registerSample(@RequestBody @Valid Sample sample) throws InterruptedException {
        log.info("sampleRequest: {}", sample);
        return new Response<String>();
    }

    @PutMapping("/{id}")
    public Response<String> saveSample(@PathVariable Long id, @RequestBody SampleRequest sampleRequest) {
        log.info("id: {}, sampleRequest: {}", id, sampleRequest);
        return new Response<String>();
    }

    @PatchMapping("/{id}")
    public Response<String> modifySample(@PathVariable Long id, @RequestBody SampleRequest sampleRequest) {
        log.info("id: {}, sampleRequest: {}", id, sampleRequest);
        return new Response<String>();
    }

    @DeleteMapping("/{id}")
    public Response<String> removeSample(@PathVariable Long id) {
        log.info("id: {}", id);
        return new Response<String>();
    }

    @GetMapping("/error")
    public Response<String> getError() {
        if (true) {
            AppException.exception(ApiError.UNKNOWN);
        }
        return new Response<String>();
    }

    /**
     * @deprecated void 유형으로 응답하면 안됩니다. Response<T> 유형으로 응답 해야합니다.
     */
    @PostMapping("/void")
    public void registerVod(@RequestBody SampleRequest sampleRequest) throws InterruptedException {
        log.info("sampleRequest: {}", sampleRequest);
    }

    @PostMapping("/display-samples")
    public Response<String> registerDisplaySample(@RequestBody SampleRequest sampleRequest) throws Exception {
        log.info("sampleRequest: {}", sampleRequest);
        return new Response<String>().setPayload(sampleService.registerDisplaySample(sampleRequest));
    }

    @GetMapping("/call-order")
    public Response<List<SampleResponse>> callOrder(SampleRequest sampleRequest) throws Exception {
        log.info("sampleRequest: {}", sampleRequest);
        return new Response<List<SampleResponse>>().setPayload(sampleService.callChain(sampleRequest));
    }

    @GetMapping("/infInfoLog")
    @IndInfoLog
    public Response<List<SampleResponse>> infInfoLog(SampleRequest sampleRequest) {
        log.info("sampleRequest: {}", sampleRequest);
        return new Response<List<SampleResponse>>().setPayload(sampleService.searchSamples(sampleRequest));
    }

    /**
     * 동영상 업로드 샘플.
     * 상품컨텐츠정보 등록 시 동영상이 업로드 되는 경우의 샘플임.
     * POST formData 로 업로드한다.
     */
    @PostMapping("/mpic")
    public Response<String> registerGoodsContInfoWithMpic(SampleMpicRequest sampleMpicRequest) {
        sampleService.registerGoodsContInfoWithMpic(sampleMpicRequest);
        return new Response<String>();
    }
}
```

{% endcode %}

***

## API Service 클래스 작성

**SampleService.java**

{% code title="SampleService.java" %}

```java
package com.x2bee.api.bo.app.service.sample;

import java.util.List;

import com.x2bee.api.bo.app.dto.request.common.SampleMpicRequest;
import com.x2bee.api.bo.app.dto.request.sample.SampleRequest;
import com.x2bee.api.bo.app.dto.response.sample.SampleResponse;

public interface SampleService {
    public List<SampleResponse> getAllSamples();
    public SampleResponse getSample(Long id);
    public List<SampleResponse> searchSamples(SampleRequest sampleRequest);
    String registerDisplaySample(SampleRequest sampleRequest) throws Exception;
    public List<SampleResponse> callChain(SampleRequest sampleRequest) throws Exception;
    public void registerGoodsContInfoWithMpic(SampleMpicRequest sampleMpicRequest);
}
```

{% endcode %}

실제 구현체

**SampleServiceImpl.java**

{% code title="SampleServiceImpl.java" %}

```java
package com.x2bee.api.bo.app.service.sample;

import java.util.List;

import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Service;

import com.x2bee.api.bo.app.dto.request.common.SampleMpicRequest;
import com.x2bee.api.bo.app.dto.request.sample.SampleRequest;
import com.x2bee.api.bo.app.dto.response.common.MpicResponse;
import com.x2bee.api.bo.app.dto.response.sample.SampleResponse;
import com.x2bee.api.bo.app.entity.PrGoodsContInfo;
import com.x2bee.api.bo.app.entity.StMpicMappInfo;
import com.x2bee.api.bo.app.repository.displayrodb.sample.SampleMapper;
import com.x2bee.api.bo.app.repository.displayrwdb.sample.SampleTrxMapper;
import com.x2bee.api.bo.app.service.common.MpicService;
import com.x2bee.api.bo.base.advice.ApiError;
import com.x2bee.common.base.exception.AppException;
import com.x2bee.common.base.rest.Response;
import com.x2bee.common.base.rest.RestApiUtil;
import com.x2bee.common.base.upload.AttacheFileKind;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@Lazy
@Slf4j
@RequiredArgsConstructor
public class SampleServiceImpl implements SampleService {

    private final SampleMapper sampleMapper;
    private final SampleTrxMapper sampleTrxMapper;
    private final RestApiUtil restApiUtil;
    private final MpicService mpicService;

    @Value("${app.apiUrl.display}")
    private String displayApiUrl;

    @Value("${app.apiUrl.order}")
    private String orderApiUrl;

    @Value("${sample.prop}")
    private String sampleProp;

    @Override
    public List<SampleResponse> getAllSamples() {
        log.debug("sample prop: {}", sampleProp);
        return sampleMapper.selectAllSamples();
    }

    @Override
    public SampleResponse getSample(Long id) {
        SampleResponse sampleResponse = sampleMapper.selectSampleById(id).orElse(null);
        if (sampleResponse == null) {
            AppException.exception(ApiError.DATA_NOT_FOUND);
        }
        return sampleResponse;
    }

    @Override
    public List<SampleResponse> searchSamples(SampleRequest sampleRequest) {
        return sampleMapper.selectSamples(sampleRequest);
    }

    @Override
    public String registerDisplaySample(SampleRequest sampleRequest) throws Exception {
        return restApiUtil.post(displayApiUrl+ "/api/display/samples", sampleRequest, new ParameterizedTypeReference<Response<String>>() {}).getPayload();
    }

    @Override
    public List<SampleResponse> callChain(SampleRequest sampleRequest) throws Exception {
        return restApiUtil.get(orderApiUrl+ "/api/order/samples/call-display", sampleRequest, new ParameterizedTypeReference<Response<List<SampleResponse>>>() {}).getPayload();
    }

    /**
     * 동영상 업로드 샘플 서비스
     */
    @Override
    public void registerGoodsContInfoWithMpic(SampleMpicRequest sampleMpicRequest) {
        // 파일 업로드
        MpicResponse mpicResponse = mpicService.uploadMpic(sampleMpicRequest.getFile1(), sampleMpicRequest.getDirection(), AttacheFileKind.GOODS);

        // 업무컨텐츠정보 등록
        PrGoodsContInfo prGoodsContInfo = insertPrGoodsContInfo(sampleMpicRequest, mpicResponse);

        // 동영상변환상태정보 등록
        registerMpicMappInfo(prGoodsContInfo, mpicResponse);
    }

    // 업무컨텐츠정보 등록 - 업무별 비지니스 로직 구현필요
    private PrGoodsContInfo insertPrGoodsContInfo(SampleMpicRequest sampleMpicRequest, MpicResponse mpicResponse) {
        PrGoodsContInfo prGoodsContInfo = new PrGoodsContInfo();
        prGoodsContInfo.setGoodsNo("TEST_001");
        prGoodsContInfo.setCmtTypCd("02");
        prGoodsContInfo.setCmtSerialNo("10"+RandomStringUtils.randomNumeric(6));
        prGoodsContInfo.setOptnCatNo("1000");
        prGoodsContInfo.setOptnNo("BLACK");
        prGoodsContInfo.setImgGbCd("T01");
        prGoodsContInfo.setBaseImgYn("N");
        prGoodsContInfo.setContFilePathNm(null);
        prGoodsContInfo.setContFileNm(mpicResponse.getFileNm());
        prGoodsContInfo.setTrnfTextCont("testTrnfTextCont");
        prGoodsContInfo.setSysRegId("FRONT");
        prGoodsContInfo.setSysModId("FRONT");
        sampleTrxMapper.insertPrGoodsContInfo(prGoodsContInfo);
        return prGoodsContInfo;
    }

    // 동영상변환상태정보 등록
    private void registerMpicMappInfo(PrGoodsContInfo prGoodsContInfo, MpicResponse mpicResponse) {
        StMpicMappInfo stMpicMappInfo = new StMpicMappInfo();

        // 원본경로명이 "/" 로 시작하도록 처리함.
        String s3OrigPathNm = StringUtils.startsWith(mpicResponse.getS3OrigPathNm(), "/") ? mpicResponse.getS3OrigPathNm() : "/" + mpicResponse.getS3OrigPathNm();
        stMpicMappInfo.setOrgPathNm(s3OrigPathNm);
        stMpicMappInfo.setTblNm("pr_goods_cont_info");
        stMpicMappInfo.setRef1Val(prGoodsContInfo.getGoodsNo());
        stMpicMappInfo.setRef2Val(prGoodsContInfo.getCmtTypCd());
        stMpicMappInfo.setRef3Val(prGoodsContInfo.getCmtSerialNo());
        stMpicMappInfo.setSysRegId("SAMPLE");
        stMpicMappInfo.setSysModId("SAMPLE");

        mpicService.registerMpicMappInfo(stMpicMappInfo);
    }
}
```

{% endcode %}

***

## Mapper 클래스 및 Query 작성

* src/main/java 하위 해당 업무 폴더에 repository/{db연결명} 폴더 생성 후 Mapper 인터페이스 작성

**SampleMapper.java**

{% code title="SampleMapper.java" %}

```java
package com.x2bee.api.bo.app.repository.displayrodb.sample;

import java.util.List;
import java.util.Optional;

import com.x2bee.api.bo.app.dto.request.sample.SampleRequest;
import com.x2bee.api.bo.app.dto.response.sample.SampleResponse;

public interface SampleMapper {
    public List<SampleResponse> selectAllSamples();
    public Optional<SampleResponse> selectSampleById(Long id);
    public List<SampleResponse> selectSamples(SampleRequest request);
}
```

{% endcode %}

* src/main/resources 하위에 해당 쿼리(XML) 작성
* namespace 에 위 Mapper 클래스 명시
* resultType 혹은 parameterType 에 데이터 객체 명시

**SampleMapper.xml**

{% code title="SampleMapper.xml" %}

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.x2bee.api.bo.app.repository.displayrodb.sample.SampleMapper">

  <sql id="sampleList">
    SELECT 1 as id, 'name1' as name, 'desc1' as description
    union all
    SELECT 2 as id, 'name2' as name, 'desc2' as description
    union all
    SELECT 3 as id, 'name3' as name, 'desc3' as description
  </sql>

  <!-- 전체 샘플 조회 -->
  <select id="selectAllSamples" resultType="sampleResponse">
    /* SampleMapper.selectAllSamples */
    <include refid="sampleList" />
  </select>

  <!-- 샘플 단건 조회 -->
  <select id="selectSampleById" parameterType="long" resultType="sampleResponse">
    /* SampleMapper.selectSampleById */
    select * from (
      <include refid="sampleList" />
    ) a where id = #{id}
  </select>

  <!-- 샘플 목록 조회 -->
  <select id="selectSamples" parameterType="sampleRequest" resultType="sampleResponse">
    /* SampleMapper.selectSamples */
    select * from (
      <include refid="sampleList" />
    ) a
    <where>
      <if test="id != null">
        and id = #{id}
      </if>
      <if test="name != null and name != ''">
        and name = #{name}
      </if>
      <if test="description != null and description != ''">
        and description = #{description}
      </if>
    </where>
  </select>

</mapper>
```

{% endcode %}

***

**위 작업 진행 시 최종 패키지 구조는 다음과 같습니다.**

<figure><img src="/files/js4r2cwJe7yidobCcy5U" alt=""><figcaption></figcaption></figure>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://tech.x2bee.com/dev-guide/dev-start/integrations/undefined-2.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
