프로젝트/[어플리케이션] 토닥

[토닥] 경계선 지능인을 위한 앱 서비스 관계 서비스 Relation-Service

마달랭 2025. 5. 27. 15:24

개요

해당 프로젝트엔 여러가지 관계가 존재한다.

경계선 지능인은 센터, 부모, 가게 및 친구들과 관계가 존재한다.

센터는 경계선 지능인과 가게에 대한 관계가 존재한다.

부모는 경계선 지능인과 관계가 존재한다.

이러한 관계 설정을 통해 웹 및 앱 서비스에서 자신과 관계가 맺어진 객체들과 상호 작용을 할 수 있다.

관계 서비스는 이러한 관계를 생성, 업데이트, 삭제하는 비즈니스 로직을 수행한다.

주체가 여러가지이기 때문에 코드가 가장 많은 서비스이다.

 

초기 기획 단계에서 부모가 경계선 지능인에 대한 관리 서비스도 기획하였으나 실제 경계선 지능인의 부모를 만나 인터뷰를 진행하지는 못했기 때문에 부모와 경계선 지능인에 대한 관계 설정은 별도로 구현하지는 않았다. 관련 내용은 생략하도록 하겠다.

 

 

프로젝트 구조



API 명세서

 

 

config.AwsS3Config.java

package com.a102.relation.config;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.Bucket;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Slf4j
@Configuration
public class AwsS3Config {

    @Value("${cloud.aws.credentials.access-key}")
    private String accessKey;

    @Value("${cloud.aws.credentials.secret-key}")
    private String secretKey;

    @Value("${cloud.aws.region.static}")
    private String region;

    @Value("${cloud.aws.s3.bucket}")
    private String bucketName;

    @Bean
    public AmazonS3 s3Client() {
        log.info("Initializing AWS S3 client with region: {}", region);

        AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

        return AmazonS3ClientBuilder.standard()
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withRegion(region)
                .build();
    }

    @PostConstruct
    public void validateAwsConfig() {
        log.info("Validating AWS configuration...");
        log.info("AWS Region: {}", region);
        log.info("AWS Access Key exists: {}", accessKey != null && !accessKey.isEmpty());
        log.info("AWS Secret Key exists: {}", secretKey != null && !secretKey.isEmpty());
        log.info("S3 Bucket name: {}", bucketName);

        // 값 앞뒤 공백 검사
        if (region != null && !region.equals(region.trim())) {
            log.warn("AWS Region contains leading or trailing whitespace!");
        }
        if (bucketName != null && !bucketName.equals(bucketName.trim())) {
            log.warn("S3 Bucket name contains leading or trailing whitespace!");
        }
    }
}

 

AWS S3를 사용하기 위해 AWS 정보를 초기화 하기 위한 설정 관련 로직이다.

관련 정보를 환경 변수로 전달 받아 초기화 작업을 수행하며 전달 받은 환경 변수가 존재 하는지, 공백 등의 오타가 있는지 여부도 검증을 진행한다.

 

 

config.OpenApiConfig.java

package com.a102.relation.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Collections;

@Configuration
public class OpenApiConfig {

    @Value("${springdoc.server-url:http://k12a102.p.ssafy.io:8080}")
    private String serverUrl;

    @Bean
    public OpenAPI openAPI() {
        Info info = new Info()
                .title("관계 서비스 API")
                .version("v1.0")
                .description("관계 관련 API 문서");

        // API Gateway URL을 서버 URL로 사용
        Server server = new Server()
                .url(serverUrl)
                .description("API Gateway URL");

        // Bearer Authentication 설정
        SecurityScheme securityScheme = new SecurityScheme()
                .type(SecurityScheme.Type.HTTP)
                .scheme("bearer")
                .bearerFormat("JWT")
                .in(SecurityScheme.In.HEADER)
                .name("Authorization");

        // 전역 보안 요구사항 설정 - 모든 API에 JWT 인증 적용
        SecurityRequirement securityRequirement = new SecurityRequirement()
                .addList("bearerAuth");

        return new OpenAPI()
                .servers(Collections.singletonList(server))
                .info(info)
                .components(new Components().addSecuritySchemes("bearerAuth", securityScheme))
                .security(Collections.singletonList(securityRequirement)); // 이 부분이 중요합니다
    }
}

 

Swagger를 활용한 관계 서비스의 API 문서 관련 설정 로직이다. API 문서의 정보와 JWT 토큰 인증 기능을 제공한다.

 

 

controller.BIUserController.java

package com.a102.relation.controller;

import com.a102.relation.dto.response.*;
import com.a102.relation.dto.request.*;
import com.a102.relation.service.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/relation/biuser")
@RequiredArgsConstructor
@Tag(name = "경계선 지능인 API", description = "경계선 지능인이 주체가 되어 호출하는 API")
public class BIUserController {
    private final RelationshipService relationshipService;
    private final StoreWorkerService storeWorkerService;

    @Operation(summary = "가게 정보 조회", description = "사용자 ID로 근무중인 가게 정보를 조회합니다.")
    @GetMapping("/getStores")
    public ResponseEntity<List<StoreWorkerResponse>> getStoresByWorker(
            @RequestHeader(value = "X-User-ID", required = false) String requestUserId) {
        List<StoreWorkerResponse> stores = storeWorkerService.getStoresByWorker(requestUserId);
        return ResponseEntity.ok(stores);
    }

    @Operation(summary = "센터 정보 조회", description = "사용자 ID로 소속된 센터 정보를 조회합니다.")
    @GetMapping("/getCenters")
    public ResponseEntity<List<CenterInfoResponse>> getCentersByBIUser(
            @RequestHeader(value = "X-User-ID", required = false) String requestUserId) {
        List<CenterInfoResponse> centers = relationshipService.getCentersByBIUser(requestUserId);
        return ResponseEntity.ok(centers);
    }

    @Operation(summary = "모든 관계 조회(서비스 관리자용 API)", description = "모든 관계를 조회합니다.(서비스 관리자용 API)")
    @GetMapping("/getAllRelationships")
    public ResponseEntity<List<RelationshipResponse>> getAllRelationships() {
        List<RelationshipResponse> relationships = relationshipService.getAllRelationships();
        return ResponseEntity.ok(relationships);
    }

    @Operation(summary = "연결된 모든 관계 조회", description = "사용자 ID로 자신에게 연결된 모든 관계를 조회합니다.")
    @GetMapping("/getRelationshipsBy")
    public ResponseEntity<List<RelationshipResponse>> getRelationshipsByUserId(
            @RequestHeader(value = "X-User-ID", required = false) String requestUserId) {
        List<RelationshipResponse> relationships = relationshipService.getRelationshipsByUserId(requestUserId);
        return ResponseEntity.ok(relationships);
    }

    @Operation(summary = "연결된 특정 관계 조회", description = "사용자 ID로 자신에게 연결된 특정 관계를 조회합니다.")
    @GetMapping("/getRelationshipsBy/{type}")
    public ResponseEntity<List<RelationshipResponse>> getRelationshipsByType(
            @PathVariable String type,
            @RequestHeader(value = "X-User-ID", required = false) String requestUserId) {
        List<RelationshipResponse> relationships = relationshipService.getRelationshipsByType(requestUserId, type);
        return ResponseEntity.ok(relationships);
    }

    @Operation(summary = "관계 생성", description = "특정 사용자와의 관계를 설정합니다.")
    @PostMapping("/createRelationship/{targetId}")
    public ResponseEntity<RelationshipResponse> createRelationship(
            @PathVariable String targetId,
            @RequestBody RelationshipRequest request,
            @RequestHeader(value = "X-User-ID", required = false) String requestUserId) {
        RelationshipResponse createdRelationship = relationshipService.createRelationship(requestUserId, targetId, request);
        return new ResponseEntity<>(createdRelationship, HttpStatus.CREATED);
    }

    @Operation(summary = "관계 삭제", description = "특정 사용자와의 관계를 삭제합니다.")
    @DeleteMapping("/deleteRelationship/{targetId}")
    public ResponseEntity<Void> deleteRelationship(
            @PathVariable String targetId,
            @RequestHeader(value = "X-User-ID", required = false) String requestUserId) {
        relationshipService.deleteRelationship(requestUserId, targetId);
        return ResponseEntity.noContent().build();
    }
}


경계선 지능인이 주체가 되어 호출하는 API에 대한 컨트롤러이다.

API 게이트웨이에서 JWT토큰 검증을 통해 요청에 X-User-ID가 헤더에 포함되어 전달되기 때문에 요청에 직접 유저 ID를 전달 받지 않기 때문에 각 메서드의 매개 변수로 X-User-ID를 전달 받게 된다.

주로 경계선 지능인과 관련된 관계에 대해 CRUD작업을 진행하는 비즈니스 로직을 호출한다.

 

 

controller.CenterController.java

package com.a102.relation.controller;

import com.a102.relation.dto.response.*;
import com.a102.relation.dto.request.*;
import com.a102.relation.service.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;

@RestController
@RequestMapping("/relation/center")
@RequiredArgsConstructor
@Tag(name = "지원 센터 API", description = "지원 센터가 주체가 되어 호출하는 API")
public class CenterController {
    private final CenterService centerService;
    private final StoreWorkerService storeWorkerService;

    @Operation(summary = "사용자 추가", description = "센터에 새로운 경계선 지능인을 소속시킵니다.")
    @PostMapping("/createUser/{token}")
    public ResponseEntity<ResultResponse> createUser(
            @PathVariable String token,
            @RequestHeader(value = "X-User-ID", required = false) String requestUserId) {
        ResultResponse result = centerService.createUser(requestUserId, token);
        return ResponseEntity.ok(result);
    }

    @Operation(summary = "사용자 삭제", description = "센터에서 특정 경계선 지능인을 삭제합니다.")
    @DeleteMapping("/deleteUser/{bId}")
    public ResponseEntity<ResultResponse> deleteUser(
            @PathVariable String bId,
            @RequestHeader(value = "X-User-ID", required = false) String requestUserId) {
        ResultResponse result = centerService.deleteUser(requestUserId, bId);
        return ResponseEntity.ok(result);
    }

    @Operation(summary = "사용자 상태 변경", description = "센터에 종속된 특정 경계선 지능인의 상태를 변경합니다.(활성화, 비활성화, 휴식 등)")
    @PutMapping("/updateUser/{bId}")
    public ResponseEntity<ResultResponse> updateUser(
            @PathVariable String bId,
            @RequestHeader(value = "X-User-ID", required = false) String requestUserId) {
        ResultResponse result = centerService.updateUser(requestUserId, bId);
        return ResponseEntity.ok(result);
    }

    @Operation(summary = "가게 목록 조회", description = "센터에 종속된 가게 목록을 반환합니다.")
    @GetMapping("/getStores")
    public ResponseEntity<List<StoreResponse>> getStoresByCenter(
            @RequestHeader(value = "X-User-ID", required = false) String requestUserId) {
        List<StoreResponse> stores = centerService.getStoresByCenter(requestUserId);
        return ResponseEntity.ok(stores);
    }

    @Operation(summary = "경계선 지능인 조회", description = "센터에 종속된 경계선 지능인 목록을 반환합니다.")
    @GetMapping("/getUsers")
    public ResponseEntity<List<UserResponse>> getUsers(
            @RequestHeader(value = "X-User-ID", required = false) String requestUserId) {
        List<UserResponse> users = centerService.getUsers(requestUserId);
        return ResponseEntity.ok(users);
    }

    @Operation(summary = "특정 가게 조회", description = "가게 ID를 기반으로 특정 가게를 조회합니다.")
    @GetMapping("/getStoreById/{storeId}")
    public ResponseEntity<StoreResponse> getStoreById(
            @PathVariable String storeId) {
        StoreResponse store = centerService.getStoreById(storeId);
        return ResponseEntity.ok(store);
    }

    @Operation(summary = "가게 등록", description = "센터에 종속된 신규 가게를 등록합니다.")
    @PostMapping(value = "/createStore", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<StoreResponse> createStore(
            @RequestHeader(value = "X-User-ID", required = false) String requestUserId,
            @RequestPart(value = "request") String requestJson,
            @RequestPart(value = "image", required = false) MultipartFile imageFile) throws IOException {

        ObjectMapper objectMapper = new ObjectMapper();
        StoreRequest request = objectMapper.readValue(requestJson, StoreRequest.class);
        StoreResponse createdStore = centerService.createStore(requestUserId, request, imageFile);
        return ResponseEntity.ok(createdStore);
    }

    @Operation(summary = "가게 삭제", description = "센터에 종속된 특정 가게와의 관계와 가게 정보를 삭제합니다.")
    @DeleteMapping("/deleteStore/{storeId}")
    public ResponseEntity<Void> deleteStore(
            @PathVariable String storeId) {
        centerService.deleteStore(storeId);
        return ResponseEntity.noContent().build();
    }

    @Operation(summary = "취업 중인 경계선 지능인 목록 조회", description = "센터에 종속된 일하고 있는 경계선 지능인 목록을 반환합니다.")
    @GetMapping("/getAllWorkers")
    public ResponseEntity<List<StoreWorkerResponse>> getAllWorkers(
            @RequestHeader(value = "X-User-ID", required = false) String requestUserId) {
        List<StoreWorkerResponse> workers = storeWorkerService.getAllWorkers(requestUserId);
        return ResponseEntity.ok(workers);
    }

    @Operation(summary = "특정 가게에 취업한 경계선 지능인 목록 조회", description = "센터에 종속된 특정 가게에서 일하고 있는 경계선 지능인 목록을 반환합니다.")
    @GetMapping("/getWorkersBy/{storeId}")
    public ResponseEntity<List<StoreWorkerResponse>> getWorkersByStore(
            @PathVariable String storeId) {
        List<StoreWorkerResponse> workers = storeWorkerService.getWorkersByStore(storeId);
        return ResponseEntity.ok(workers);
    }

    @Operation(summary = "특정 가게에 취업한 경계선 지능인 조회", description = "센터에 종속된 특정 가게에서 일하고 있는 경계선 지능인을 반환합니다.")
    @GetMapping("/getWorkerBy/{storeId}/{bId}")
    public ResponseEntity<StoreWorkerResponse> getWorkerById(
            @PathVariable String storeId,
            @PathVariable String bId) {
        StoreWorkerResponse worker = storeWorkerService.getWorkerById(storeId, bId);
        return ResponseEntity.ok(worker);
    }

    @Operation(summary = "특정 가게에 경계선 지능인 등록", description = "센터에 종속된 특정 가게에 경계선 지능인을 추가합니다.")
    @PostMapping("/createWorker/{storeId}")
    public ResponseEntity<StoreWorkerResponse> createWorker(
            @PathVariable String storeId,
            @RequestBody StoreWorkerRequest request) {
        StoreWorkerResponse createdWorker = storeWorkerService.createWorker(storeId, request);
        return new ResponseEntity<>(createdWorker, HttpStatus.CREATED);
    }

    @Operation(summary = "특정 가게에 경계선 지능인 상태 변경", description = "센터에 종속된 특정 가게에 경계선 지능인의 상태를 변경합니다.(취업, 휴직 등)")
    @PutMapping("/updateWorker/{storeId}")
    public ResponseEntity<StoreWorkerResponse> updateWorker(
            @PathVariable String storeId,
            @RequestBody StoreWorkerRequest request) {
        StoreWorkerResponse updatedWorker = storeWorkerService.updateWorker(storeId, request);
        return ResponseEntity.ok(updatedWorker);
    }

    @Operation(summary = "특정 가게에 경계선 지능인 모두 삭제", description = "센터에 종속된 특정 가게에 경계선 지능인을 모두 삭제합니다.")
    @DeleteMapping("/deleteWorker/{storeId}")
    public ResponseEntity<Void> deleteWorker(
            @PathVariable String storeId) {
        storeWorkerService.deleteWorker(storeId);
        return ResponseEntity.noContent().build();
    }

    @Operation(summary = "특정 가게에 특정 경계선 지능인 삭제", description = "센터에 종속된 특정 가게에 특정 경계선 지능인을 삭제합니다.")
    @DeleteMapping("/deleteWorker/{storeId}/{userId}")
    public ResponseEntity<Void> deleteWorkerByStoreAndUser(
            @PathVariable String storeId,
            @PathVariable String userId) {
        storeWorkerService.deleteWorkerByStoreAndUser(storeId, userId);
        return ResponseEntity.noContent().build();
    }
}


센터가 주가 되어 호출하는 API를 제공하는 컨트롤러이다.

마찬가지로 API 게이트웨이에서 X-User-ID를 제공하기 때문에 요청의 헤더에서 userID정보를 파싱하여 매개변수로 전달하여 사용한다. 센터 혹은 센터와 관련된 경계선 지능인과 가게에 대한 CRUD 비즈니스 로직을 수행한다.

특이 사항으로 가게는 이미지를 전달 받기 때문에 가게 생성 시 대한 appliaction/json이 아닌 Mutipart form data형식으로 데이터를 받아 주어야 한다.

 

 

dto.request.RelationshipRequest.java

package com.a102.relation.dto.request;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RelationshipRequest {
    private String type;  // 관계 유형 (예: "friend", "parent", "center" 등)
    private String status; // 상태 (예: "pending", "accepted", "active" 등)
    private String relationType; // 부모-자녀 관계 유형 (예: "biological", "adoptive" 등)
    private String role; // BIStore 관계에서의 역할
}

 

관계 생성 요청에 대한 DTO

 

 

dto.request.StoreRequest.java

package com.a102.relation.dto.request;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class StoreRequest {
    private String name;
    private String address;
    private String phone;
    private String description;
}


가게 생성에 대한 DTO

 

 

dto.request.StoreWorkerRequest.java

package com.a102.relation.dto.request;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDate;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class StoreWorkerRequest {
    private String biUserId;
    private String role;
    private LocalDate startDate;
    private LocalDate endDate;
    private String status;  // "active", "inactive", "ended"
}

 

가게에 경계선 지능인을 추가하기 위한 DTO

 

 

dto.response.CenterInfoResponse.java

package com.a102.relation.dto.response;

import com.a102.relation.entity.Center;
import com.a102.relation.entity.User;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDate;

@Getter
@NoArgsConstructor
public class CenterInfoResponse {
    private String centerId;
    private String centerName;
    private String address;
    private String phone;
    private String description;
    private String status;
    private LocalDate startDate;
    private LocalDate endDate;

    @Builder
    public CenterInfoResponse(String centerId, String centerName, String address, String phone,
                              String description, String status, LocalDate startDate, LocalDate endDate) {
        this.centerId = centerId;
        this.centerName = centerName;
        this.address = address;
        this.phone = phone;
        this.description = description;
        this.status = status;
        this.startDate = startDate;
        this.endDate = endDate;
    }

    public static CenterInfoResponse fromEntity(Center center, User user, String status,
                                                LocalDate startDate, LocalDate endDate) {
        return CenterInfoResponse.builder()
                .centerId(center.getUserId())
                .centerName(user.getName())
                .address(user.getAddress())
                .phone(user.getPhone())
                .description(center.getDescription())
                .status(status)
                .startDate(startDate)
                .endDate(endDate)
                .build();
    }
}

 

센터 정보를 반환하는 DTO, 생성자가 존재하지만 스키마 구조가 정규화가 많이 진행 되어있는 상태이므로 Join연산이 많은 편이다. 따라서 실제 비즈니스 로직에선 엔티티 두개를 매개변수로 받아와 DTO를 빌드하는 메서드를 사용하였다.

 

 

dto.response.ErrorResponse.java

package com.a102.relation.dto.response;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@NoArgsConstructor
public class ErrorResponse {
    private String errorCode;
    private String message;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime timestamp;

    public ErrorResponse(String errorCode, String message) {
        this.errorCode = errorCode;
        this.message = message;
        this.timestamp = LocalDateTime.now();
    }
}

 

런타임 환경에서 예외 발생 시 해당 예외를 클라이언트에게 전달하기 위한 DTO

 

 

dto.response.RelationshipResponse.java

package com.a102.relation.dto.response;

import com.a102.relation.entity.BiFriend;
import com.a102.relation.entity.ParentChild;
import com.a102.relation.entity.CenterBi;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDate;
import java.time.LocalDateTime;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RelationshipResponse {
    private String id;
    private String sourceUserId;
    private String sourceName;
    private String sourceType;  // "bid", "parent", "center"

    private String targetUserId;
    private String targetName;
    private String targetType;  // "bid", "parent", "center"

    private String relationshipType;  // "friend", "parent-child", "center-bi" 등
    private String relationshipStatus;  // "pending", "accepted", "rejected" 등
    private String status;  // "active", "inactive", "ended" 등

    private String relationType;  // ParentChild에서 사용하는 관계 유형
    private String role;  // BIStore에서 사용하는 역할

    private LocalDate startDate;
    private LocalDate endDate;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    // BIFriend 엔티티로부터 응답 객체 생성
    public static RelationshipResponse fromBIFriend(BiFriend biFriend, String sourceUserId) {
        boolean isSource = biFriend.getBiUserId1().equals(sourceUserId);

        return RelationshipResponse.builder()
                .id(biFriend.getId())
                .sourceUserId(isSource ? biFriend.getBiUserId1() : biFriend.getBiUserId2())
                .sourceName(isSource ? biFriend.getBiUser1().getUser().getName() : biFriend.getBiUser2().getUser().getName())
                .sourceType("bid")
                .targetUserId(isSource ? biFriend.getBiUserId2() : biFriend.getBiUserId1())
                .targetName(isSource ? biFriend.getBiUser2().getUser().getName() : biFriend.getBiUser1().getUser().getName())
                .targetType("bid")
                .relationshipType("friend")
                .relationshipStatus(biFriend.getRelationshipStatus())
                .status(biFriend.getStatus())
                .endDate(biFriend.getEndDate())
                .createdAt(biFriend.getCreatedAt())
                .updatedAt(biFriend.getUpdatedAt())
                .build();
    }

    // ParentChild 엔티티로부터 응답 객체 생성
    public static RelationshipResponse fromParentChild(ParentChild parentChild, boolean asParent) {
        if (asParent) {
            return RelationshipResponse.builder()
                    .id(parentChild.getId())
                    .sourceUserId(parentChild.getParentUserId())
                    .sourceName(parentChild.getParent().getUser().getName())
                    .sourceType("parent")
                    .targetUserId(parentChild.getChildUserId())
                    .targetName(parentChild.getChild().getUser().getName())
                    .targetType("bid")
                    .relationshipType("parent-child")
                    .relationType(parentChild.getRelationshipType())
                    .status(parentChild.getStatus())
                    .createdAt(parentChild.getCreatedAt())
                    .updatedAt(parentChild.getUpdatedAt())
                    .build();
        } else {
            return RelationshipResponse.builder()
                    .id(parentChild.getId())
                    .sourceUserId(parentChild.getChildUserId())
                    .sourceName(parentChild.getChild().getUser().getName())
                    .sourceType("bid")
                    .targetUserId(parentChild.getParentUserId())
                    .targetName(parentChild.getParent().getUser().getName())
                    .targetType("parent")
                    .relationshipType("parent-child")
                    .relationType(parentChild.getRelationshipType())
                    .status(parentChild.getStatus())
                    .createdAt(parentChild.getCreatedAt())
                    .updatedAt(parentChild.getUpdatedAt())
                    .build();
        }
    }

    // CenterBi 엔티티로부터 응답 객체 생성
    public static RelationshipResponse fromCenterBI(CenterBi CenterBi, boolean asCenter) {
        if (asCenter) {
            return RelationshipResponse.builder()
                    .id(CenterBi.getId())
                    .sourceUserId(CenterBi.getCenterUserId())
                    .sourceName(CenterBi.getCenter().getUser().getName())
                    .sourceType("center")
                    .targetUserId(CenterBi.getBiUserId())
                    .targetName(CenterBi.getBiUser().getUser().getName())
                    .targetType("bid")
                    .relationshipType("center-bi")
                    .status(CenterBi.getStatus())
                    .startDate(CenterBi.getStartDate())
                    .endDate(CenterBi.getEndDate())
                    .createdAt(CenterBi.getCreatedAt())
                    .updatedAt(CenterBi.getUpdatedAt())
                    .build();
        } else {
            return RelationshipResponse.builder()
                    .id(CenterBi.getId())
                    .sourceUserId(CenterBi.getBiUserId())
                    .sourceName(CenterBi.getBiUser().getUser().getName())
                    .sourceType("bid")
                    .targetUserId(CenterBi.getCenterUserId())
                    .targetName(CenterBi.getCenter().getUser().getName())
                    .targetType("center")
                    .relationshipType("center-bi")
                    .status(CenterBi.getStatus())
                    .startDate(CenterBi.getStartDate())
                    .endDate(CenterBi.getEndDate())
                    .createdAt(CenterBi.getCreatedAt())
                    .updatedAt(CenterBi.getUpdatedAt())
                    .build();
        }
    }
}

 

두 객체간 관계 테이블은 두 객체 모두 동일한 테이블을 참조하게 된다. 하지만 다대다 관계가 성립하기 때문에 주체에 따라 반환값은 달라질 수 있다. 그렇기 때문에 모든 관계에 대한 응답은 같은 테이블을 참조하되 현재 객체의 type을 매개변수로 전달하여 주 객체를 식별하고 알맞게 DTO를 빌드하여 응답 요청을 반환한다.

 

 

dto.response.ResultResponse.java

package com.a102.relation.dto.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ResultResponse {
    private boolean success;
    private String message;
    private Object data;

    public static ResultResponse success(String message) {
        return ResultResponse.builder()
                .success(true)
                .message(message)
                .build();
    }

    public static ResultResponse success(String message, Object data) {
        return ResultResponse.builder()
                .success(true)
                .message(message)
                .data(data)
                .build();
    }

    public static ResultResponse error(String message) {
        return ResultResponse.builder()
                .success(false)
                .message(message)
                .build();
    }
}

 

별도의 데이터 응답이 없이 요청에 대한 성공 여부를 반환하기 위한 DTO

 

 

dto.response.StoreResponse.java

package com.a102.relation.dto.response;

import com.a102.relation.entity.Store;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class StoreResponse {
    private String id;
    private String name;
    private String address;
    private String phone;
    private String image;
    private String description;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;;

    // Store 엔티티로부터 응답 객체 생성하는 정적 메서드
    public static StoreResponse fromEntity(Store store) {
        return StoreResponse.builder()
                .id(store.getId())
                .name(store.getName())
                .address(store.getAddress())
                .phone(store.getPhone())
                .image(store.getImage())
                .description(store.getDescription())
                .createdAt(store.getCreatedAt())
                .updatedAt(store.getUpdatedAt())
                .build();
    }
}

 

가게 정보를 반환하는 DTO

 

 

dto.response.StoreWorkerResponse.java

package com.a102.relation.dto.response;

import com.a102.relation.entity.BiStore;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDate;
import java.time.LocalDateTime;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class StoreWorkerResponse {
    private String id;
    private String biUserId;
    private String biUserName;
    private String storeId;
    private String storeName;
    private String role;
    private String status;
    private LocalDate startDate;
    private LocalDate endDate;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    // BIStore 엔티티로부터 응답 객체 생성
    public static StoreWorkerResponse fromEntity(BiStore biStore) {
        return StoreWorkerResponse.builder()
                .id(biStore.getId())
                .biUserId(biStore.getBiUserId())
                .biUserName(biStore.getBiUser().getUser().getName())
                .storeId(biStore.getStoreId())
                .storeName(biStore.getStore().getName())
                .role(biStore.getRole())
                .status(biStore.getStatus())
                .startDate(biStore.getStartDate())
                .endDate(biStore.getEndDate())
                .createdAt(biStore.getCreatedAt())
                .updatedAt(biStore.getUpdatedAt())
                .build();
    }
}

 

가게에 소속된 유저를 반환하는 DTO

 

 

 

dto.response.UserResponse.java

package com.a102.relation.dto.response;

import com.a102.relation.entity.BiUser;
import com.a102.relation.entity.Store;
import com.a102.relation.entity.User;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserResponse {
    private String id;
    private String name;
    private String image;
    private String needs;

    // Store 엔티티로부터 응답 객체 생성하는 정적 메서드
    public static UserResponse fromEntity(BiUser biUser, User user) {
        return UserResponse.builder()
                .id(user.getId())
                .name(user.getName())
                .image(biUser.getImage())
                .needs(biUser.getSpecialNeeds())
                .build();
    }
}

 

경계선 지능인 유저 정보를 반환하는 DTO, 이미지 URL이 필요하기 때문에 User테이블과 BiUser테이블 Join이 필요하다.

 

 

entity.BiFriend.java

package com.a102.relation.entity;

import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDate;
import java.time.LocalDateTime;

@Entity
@Table(name = "BiFriend")
@Getter @Setter @NoArgsConstructor
public class BiFriend {
    @Id
    @Column(name = "id")
    private String id;

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "bi_user_id1", insertable = false, updatable = false)
    private BiUser biUser1;

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "bi_user_id2", insertable = false, updatable = false)
    private BiUser biUser2;

    @Column(name = "rel_status")
    private String relationshipStatus;

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

    @Column(name = "end_date")
    private LocalDate endDate;

    @Column(name = "created")
    private LocalDateTime createdAt;

    @Column(name = "updated")
    private LocalDateTime updatedAt;

    public enum RelationshipStatus {
        pending, accepted, rejected
    }

    @PrePersist
    protected void onCreate() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        this.updatedAt = LocalDateTime.now();
    }

    @Builder
    public BiFriend(String id, String biUserId1, String biUserId2,
                    String relationshipStatus, String status, LocalDate endDate) {
        this.id = id;
        this.biUserId1 = biUserId1;
        this.biUserId2 = biUserId2;
        this.relationshipStatus = relationshipStatus;
        this.status = status;
        this.endDate = endDate;
    }
}

 

경계선 지능인간 관계 테이블 관련 엔티티

 

 

entity.BiStore.java

package com.a102.relation.entity;

import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDate;
import java.time.LocalDateTime;

@Entity
@Table(name = "BiStore")
@Getter @Setter @NoArgsConstructor
public class BiStore {
    @Id
    @Column(name = "id")
    private String id;

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "bi_user_id", insertable = false, updatable = false)
    private BiUser biUser;

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "store_id", insertable = false, updatable = false)
    private Store store;

    @Column(name = "start_date", nullable = false)
    private LocalDate startDate;

    @Column(name = "end_date")
    private LocalDate endDate;

    @Column(name = "role", length = 100)
    private String role;

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

    @Column(name = "created")
    private LocalDateTime createdAt;

    @Column(name = "updated")
    private LocalDateTime updatedAt;

    @PrePersist
    protected void onCreate() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        this.updatedAt = LocalDateTime.now();
    }

    @Builder
    public BiStore(String id, String biUserId, String storeId,
                   LocalDate startDate, LocalDate endDate, String role, String status) {
        this.id = id;
        this.biUserId = biUserId;
        this.storeId = storeId;
        this.startDate = startDate;
        this.endDate = endDate;
        this.role = role;
        this.status = status;
    }
}

 

경계선 지능인과 가게 관계 테이블 관련 엔티티

 

 

 

 

entity.BiUser.java

package com.a102.relation.entity;

import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDate;
import java.time.LocalDateTime;

@Entity
@Table(name = "BiUser")
@Getter @Setter @NoArgsConstructor
public class BiUser {
    @Id
    @Column(name = "user_id")
    private String userId;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", insertable = false, updatable = false)
    private User user;

    @Column(name = "birth")
    private String birthDate;

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

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

    @Column(name = "needs")
    private String specialNeeds;

    @Column(name = "created")
    private LocalDateTime createdAt;

    @Column(name = "updated")
    private LocalDateTime updatedAt;

    @PrePersist
    protected void onCreate() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        this.updatedAt = LocalDateTime.now();
    }

    @Builder
    public BiUser(String userId, String birthDate, String gender, String specialNeeds) {
        this.userId = userId;
        this.birthDate = birthDate;
        this.gender = gender;
        this.specialNeeds = specialNeeds;
    }
}

 

경계선 지능인 테이블 관련 엔티티

 

 

entity.Center.java

package com.a102.relation.entity;

import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDateTime;

@Entity
@Table(name = "Center")
@Getter @Setter @NoArgsConstructor
public class Center {
    @Id
    @Column(name = "user_id")
    private String userId;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", insertable = false, updatable = false)
    private User user;

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

    @Column(name = "created")
    private LocalDateTime createdAt;

    @Column(name = "updated")
    private LocalDateTime updatedAt;

    @PrePersist
    protected void onCreate() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        this.updatedAt = LocalDateTime.now();
    }

    @Builder
    public Center(String userId, String description) {
        this.userId = userId;
        this.description = description;
    }
}

 

센터 테이블 관련 엔티티

 

 

entity.CenterBi.java

package com.a102.relation.entity;

import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDate;
import java.time.LocalDateTime;

@Entity
@Table(name = "CenterBi")
@Getter @Setter @NoArgsConstructor
public class CenterBi {
    @Id
    @Column(name = "id")
    private String id;

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "center_user_id", insertable = false, updatable = false)
    private Center center;

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "bi_user_id", insertable = false, updatable = false)
    private BiUser biUser;

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

    @Column(name = "start_date", nullable = false)
    private LocalDate startDate;

    @Column(name = "end_date")
    private LocalDate endDate;

    @Column(name = "created")
    private LocalDateTime createdAt;

    @Column(name = "updated")
    private LocalDateTime updatedAt;

    @PrePersist
    protected void onCreate() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        this.updatedAt = LocalDateTime.now();
    }

    @Builder
    public CenterBi(String id, String centerUserId, String biUserId,
                    String status, LocalDate startDate, LocalDate endDate) {
        this.id = id;
        this.centerUserId = centerUserId;
        this.biUserId = biUserId;
        this.status = status;
        this.startDate = startDate;
        this.endDate = endDate;
    }
}

 

센터와 경계선 지능인 관계 테이블 관련 엔티티

 

 

entity.CenterStore.java

package com.a102.relation.entity;

import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDate;
import java.time.LocalDateTime;

@Entity
@Table(name = "CenterStore")
@Getter @Setter @NoArgsConstructor
public class CenterStore {
    @Id
    @Column(name = "id")
    private String id;

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "center_user_id", insertable = false, updatable = false)
    private Center center;

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "store_id", insertable = false, updatable = false)
    private Store store;

    @Column(name = "start_date", nullable = false)
    private LocalDate startDate;

    @Column(name = "end_date")
    private LocalDate endDate;

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

    @Column(name = "created")
    private LocalDateTime createdAt;

    @Column(name = "updated")
    private LocalDateTime updatedAt;

    @PrePersist
    protected void onCreate() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        this.updatedAt = LocalDateTime.now();
    }

    @Builder
    public CenterStore(String id, String centerUserId, String storeId,
                       LocalDate startDate, LocalDate endDate, String status) {
        this.id = id;
        this.centerUserId = centerUserId;
        this.storeId = storeId;
        this.startDate = startDate;
        this.endDate = endDate;
        this.status = status;
    }
}

 

센터와 가게 관계 테이블 관련 엔티티

 

 

entity.Store.java

package com.a102.relation.entity;

import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDateTime;

@Entity
@Table(name = "Store")
@Getter @Setter @NoArgsConstructor
public class Store {
    @Id
    @Column(name = "id")
    private String id;

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

    @Column(name = "addr")
    private String address;

    @Column(name = "phone", length = 20)
    private String phone;

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

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

    @Column(name = "created")
    private LocalDateTime createdAt;

    @Column(name = "updated")
    private LocalDateTime updatedAt;

    @PrePersist
    protected void onCreate() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        this.updatedAt = LocalDateTime.now();
    }

    @Builder
    public Store(String id, String name, String address, String phone,
                 String image, String description) {
        this.id = id;
        this.name = name;
        this.address = address;
        this.phone = phone;
        this.image = image;
        this.description = description;
    }
}

 

가게 테이블 관련 엔티티

 

 

entity.BiStore.java

package com.a102.relation.entity;

import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDateTime;

@Entity
@Table(name = "User")
@Getter @Setter @NoArgsConstructor
public class User {
    @Id
    @Column(name = "id")
    private String id;

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

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

    @Column(name = "email", unique = true, length = 100)
    private String email;

    @Column(name = "pwd", nullable = false, length = 255)
    private String password;

    @Column(name = "phone", length = 20)
    private String phone;

    @Column(name = "addr")
    private String address;

    @Column(name = "created")
    private LocalDateTime createdAt;

    @Column(name = "updated")
    private LocalDateTime updatedAt;

    @PrePersist
    protected void onCreate() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        this.updatedAt = LocalDateTime.now();
    }

    @Builder
    public User(String id, String type, String name, String email, String password,
                String phone, String address) {
        this.id = id;
        this.type = type;
        this.name = name;
        this.email = email;
        this.password = password;
        this.phone = phone;
        this.address = address;
    }
}

 

유저 테이블 관련 엔티티

 

 

exception.GlobalExceptionHandler.java

package com.a102.relation.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import com.a102.relation.dto.response.*;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
        log.warn("요소를 찾지 못함: {}", ex.getMessage());
        ErrorResponse errorResponse = new ErrorResponse(
                "NOT_FOUND",
                ex.getMessage()
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(RelationshipAlreadyExistsException.class)
    public ResponseEntity<ErrorResponse> handleRelationshipAlreadyExistsException(RelationshipAlreadyExistsException ex, WebRequest request) {
        log.warn("관계가 이미 성립되어 있음: {}", ex.getMessage());
        ErrorResponse errorResponse = new ErrorResponse(
                "CONFLICT",
                ex.getMessage()
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT);
    }

    @ExceptionHandler(InvalidTokenException.class)
    public ResponseEntity<ErrorResponse> handleInvalidTokenException(InvalidTokenException ex, WebRequest request) {
        log.warn("유효하지 않은, 만료되었거나 손상된 토큰: {}", ex.getMessage());
        ErrorResponse errorResponse = new ErrorResponse(
                "UNAUTHORIZED",
                ex.getMessage()
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.UNAUTHORIZED);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException ex, WebRequest request) {
        log.warn("유효성 검증 예외 발생: {}", ex.getMessage());
        ErrorResponse errorResponse = new ErrorResponse(
                "BAD_REQUEST",
                ex.getMessage()
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGlobalException(Exception ex, WebRequest request) {
        log.error("처리되지 않은 예외 발생: ", ex);
        // 스택 트레이스 전체를 로깅
        log.error("예외 상세 정보: ", ex);
        // 예외 발생 위치 추적을 위한 추가 정보
        StackTraceElement[] stackTrace = ex.getStackTrace();
        if (stackTrace.length > 0) {
            log.error("예외 발생 위치: {}.{} ({}:{})",
                    stackTrace[0].getClassName(),
                    stackTrace[0].getMethodName(),
                    stackTrace[0].getFileName(),
                    stackTrace[0].getLineNumber());
        }
        ErrorResponse errorResponse = new ErrorResponse(
                "INTERNAL_SERVER_ERROR",
                ex.getMessage()
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

 

런타임 도중 발생할 수 있는 예외 처리를 위한 핸들링 로직이다. 예외 발생 시 서버 로그와 클라이언트에게 예외에 대한 정보를 반환한다, 해당 핸들러에 예외 정보가 존재하므로 별도 예외에 대한 내용은 생략하도록 하겠다.

 

 

repository

레포지토리 또한 JPA기반의 트랜잭션 처리를 진행하므로 생략하도록 하겠다.

 

 

service.CenterService.java

package com.a102.relation.service;

import com.a102.relation.dto.request.*;
import com.a102.relation.dto.response.*;
import com.a102.relation.entity.*;
import com.a102.relation.exception.ResourceNotFoundException;
import com.a102.relation.exception.InvalidTokenException;
import com.a102.relation.exception.RelationshipAlreadyExistsException;
import com.a102.relation.repository.*;
import com.a102.relation.util.UserContextUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class CenterService {

    private final UserRepository userRepository;
    private final CenterRepository centerRepository;
    private final BIUserRepository biUserRepository;
    private final CenterBIRepository centerBIRepository;
    private final StoreRepository storeRepository;
    private final CenterStoreRepository centerStoreRepository;
    private final UserContextUtil userContextUtil;
    private final S3Service s3Service;
    private final TokenService tokenService;

    /**
     * 센터-경계선 지능인 관계를 생성합니다.
     */
    @Transactional
    public ResultResponse createUser(String CID, String token) {
        // API 게이트웨이에서 받은 사용자 ID 검증
        String centerUserId = userContextUtil.validateAndGetUserId(CID);

        // 센터 사용자인지 확인
        User centerUser = userRepository.findById(centerUserId)
                .orElseThrow(() -> new ResourceNotFoundException("Center user", "id", centerUserId));

        if (!centerUser.getType().equals("center")) {  // String 비교로 수정
            throw new IllegalArgumentException("User is not a center type");
        }

        String biUserId = tokenService.validateToken(token);
        if (biUserId == null) {
            throw new IllegalArgumentException("Invalid token");
        }

        // 경계선 지능인 사용자인지 확인
        User biUser = userRepository.findById(biUserId)
                .orElseThrow(() -> new ResourceNotFoundException("BI user", "id", biUserId));

        if (!biUser.getType().equals(("bid"))) {
            throw new IllegalArgumentException("User is not a BI type");
        }

        // 이미 관계가 있는지 확인
        boolean existingRelationship = centerBIRepository.existsByCenterUserIdAndBiUserId(centerUserId, biUserId);

        if (existingRelationship) {
            throw new RelationshipAlreadyExistsException("Center-BI", centerUserId, biUserId);
        }

        CenterBi centerbi = CenterBi.builder()
                .id(UUID.randomUUID().toString())
                .centerUserId(centerUserId)
                .biUserId(biUserId)
                .startDate(LocalDate.now())
                .status("active")  // Enum -> String
                .build();

        centerBIRepository.save(centerbi);

        return ResultResponse.success("Relationship created successfully between center and BI user");
    }

    /**
     * 센터-경계선 지능인 관계를 삭제합니다.
     */
    @Transactional
    public ResultResponse deleteUser(String CID, String biUserId) {
        // API 게이트웨이에서 받은 사용자 ID 검증
        String centerUserId = userContextUtil.validateAndGetUserId(CID);

        // 센터 사용자인지 확인
        User centerUser = userRepository.findById(centerUserId)
                .orElseThrow(() -> new ResourceNotFoundException("Center user", "id", centerUserId));

        if (!centerUser.getType().equals("center")) {  // String 비교로 수정
            throw new IllegalArgumentException("User is not a center type");
        }

        // 관계가 있는지 확인
        CenterBi CenterBi = centerBIRepository.findByCenterUserIdAndBiUserId(centerUserId, biUserId)
                .orElseThrow(() -> new ResourceNotFoundException("Relationship", "between center and BI user",
                        centerUserId + " and " + biUserId));

        centerBIRepository.delete(CenterBi);

        return ResultResponse.success("Relationship deleted successfully between center and BI user");
    }

    /**
     * 센터-경계선 지능인 관계를 업데이트합니다.
     */
    @Transactional
    public ResultResponse updateUser(String CID, String biUserId) {
        // API 게이트웨이에서 받은 사용자 ID 검증
        String centerUserId = userContextUtil.validateAndGetUserId(CID);

        // 센터 사용자인지 확인
        User centerUser = userRepository.findById(centerUserId)
                .orElseThrow(() -> new ResourceNotFoundException("Center user", "id", centerUserId));

        if (!centerUser.getType().equals("center")) {  // String 비교로 수정
            throw new IllegalArgumentException("User is not a center type");
        }

        // 관계가 있는지 확인
        CenterBi CenterBi = centerBIRepository.findByCenterUserIdAndBiUserId(centerUserId, biUserId)
                .orElseThrow(() -> new ResourceNotFoundException("Relationship", "between center and BI user",
                        centerUserId + " and " + biUserId));

        if ("active".equals(CenterBi.getStatus())) {  // Enum 비교 -> String 비교
            CenterBi.setStatus("inactive");
        } else if ("inactive".equals(CenterBi.getStatus())) {  // Enum 비교 -> String 비교
            CenterBi.setStatus("active");
        } else {
            // 종료된 관계는 변경 불가
            return ResultResponse.error("Cannot update ended relationship");
        }

        centerBIRepository.save(CenterBi);

        return ResultResponse.success("Relationship updated successfully between center and BI user");
    }

    /**
     * 센터가 관리하는 모든 상점을 조회합니다.
     */
    @Transactional(readOnly = true)
    public List<StoreResponse> getStoresByCenter(String CID) {
        // API 게이트웨이에서 받은 사용자 ID 검증
        String centerUserId = userContextUtil.validateAndGetUserId(CID);

        // 센터 사용자인지 확인
        User centerUser = userRepository.findById(centerUserId)
                .orElseThrow(() -> new ResourceNotFoundException("Center user", "id", centerUserId));

        if (!centerUser.getType().equals("center")) {  // String 비교로 수정
            throw new IllegalArgumentException("User is not a center type");
        }

        // 센터와 연결된 모든 상점 조회
        List<CenterStore> centerStores = centerStoreRepository.findByCenterUserIdAndStatus(
                centerUserId, "active");  // Enum -> String

        // 상점 정보를 응답 객체로 변환
        return centerStores.stream()
                .map(centerStore -> {
                    Store store = storeRepository.findById(centerStore.getStoreId())
                            .orElseThrow(() -> new ResourceNotFoundException("Store", "id", centerStore.getStoreId()));
                    return StoreResponse.fromEntity(store);
                })
                .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public List<UserResponse> getUsers(String CID) {
        // API 게이트웨이에서 받은 사용자 ID 검증
        String centerUserId = userContextUtil.validateAndGetUserId(CID);

        // 센터 사용자인지 확인
        User centerUser = userRepository.findById(centerUserId)
                .orElseThrow(() -> new ResourceNotFoundException("Center user", "id", centerUserId));

        if (!centerUser.getType().equals("center")) {  // String 비교로 수정
            throw new IllegalArgumentException("User is not a center type");
        }

        // 센터와 연결된 모든 경계선 지능인 사용자 조회 (active 상태인 관계만)
        List<CenterBi> centerBIs = centerBIRepository.findByCenterUserIdAndStatus(
                centerUserId, "active");

        // 각 경계선 지능인 정보와 User 정보를 조합하여 응답 객체로 변환
        return centerBIs.stream()
                .map(CenterBi -> {
                    // 경계선 지능인 기본 정보 조회
                    User biUser = userRepository.findById(CenterBi.getBiUserId())
                            .orElseThrow(() -> new ResourceNotFoundException("BI user", "id", CenterBi.getBiUserId()));

                    // BiUser 추가 정보 조회
                    BiUser biUserDetail = biUserRepository.findById(CenterBi.getBiUserId())
                            .orElseThrow(() -> new ResourceNotFoundException("BI user detail", "id", CenterBi.getBiUserId()));

                    // UserResponse 객체 생성
                    return UserResponse.fromEntity(biUserDetail, biUser);
                })
                .collect(Collectors.toList());
    }

    /**
     * 특정 상점을 ID로 조회합니다.
     */
    @Transactional(readOnly = true)
    public StoreResponse getStoreById(String storeId) {
        Store store = storeRepository.findById(storeId)
                .orElseThrow(() -> new ResourceNotFoundException("Store", "id", storeId));

        return StoreResponse.fromEntity(store);
    }

    /**
     * 새로운 상점을 생성합니다.
     */
    @Transactional
    public StoreResponse createStore(String CID, StoreRequest request, MultipartFile imageFile) {
        // API 게이트웨이에서 받은 사용자 ID 검증
        String centerUserId = userContextUtil.validateAndGetUserId(CID);

        // 센터 사용자인지 확인
        User centerUser = userRepository.findById(centerUserId)
                .orElseThrow(() -> new ResourceNotFoundException("Center user", "id", centerUserId));

        if (!centerUser.getType().equals("center")) {  // String 비교로 수정
            throw new IllegalArgumentException("User is not a center type");
        }

        // 새 상점 생성
        String storeId = UUID.randomUUID().toString();
        Store store = Store.builder()
                .id(storeId)
                .name(request.getName())
                .address(request.getAddress())
                .phone(request.getPhone())
                .description(request.getDescription())
                .build();

        // 이미지 파일이 있으면 S3에 업로드
        if (imageFile != null && !imageFile.isEmpty()) {
            try {
                String imageUrl = s3Service.uploadImage(imageFile, storeId);
                store.setImage(imageUrl);
            } catch (RuntimeException e) {
                throw new RuntimeException("이미지 업로드 중 오류가 발생했습니다.", e);
            }
        }

        Store savedStore = storeRepository.save(store);

        // 센터-상점 관계 생성
        CenterStore centerStore = CenterStore.builder()
                .id(UUID.randomUUID().toString())
                .centerUserId(centerUserId)
                .storeId(storeId)
                .startDate(LocalDate.now())
                .status("active")  // Enum -> String
                .build();

        centerStoreRepository.save(centerStore);

        return StoreResponse.fromEntity(savedStore);
    }

    /**
     * 상점을 삭제합니다.
     */
    @Transactional
    public void deleteStore(String storeId) {
        Store store = storeRepository.findById(storeId)
                .orElseThrow(() -> new ResourceNotFoundException("Store", "id", storeId));

        // 연관된 CenterStore 관계 삭제
        List<CenterStore> centerStores = centerStoreRepository.findByStoreId(storeId);
        centerStoreRepository.deleteAll(centerStores);

        // 연관된 BIStore 관계 삭제 (StoreWorkerService에서 처리할 수도 있음)

        // 상점 삭제
        storeRepository.delete(store);
    }
}

 

센터와 관련된 비즈니스 로직들이다. 

사용자 ID를 검증하고, 현재 유저가 센터 사용자 인지 확인한다. 대상이 경계선 지능인 인지 확인 하고 이미 현재 센터와 관계가 있는지 확인 등에 대한 내용을 검증한 후에 센터와 경계선 지능인의 관계를 설정해 준다. 생성 작업 외에도 RUD작업이 존재하며 각 로직마다 예외가 발생할 수 있는 내용을 검증해주고 트랜잭션을 진행한다.

경계선 지능인과 마찬가지로 가게 정보도 검증 후 CRUD 트랜잭션을 수행한다.

특이점으로 센터에서 경계선지능인을 추가할 때 Redis가 관여된다. 사용자가 TTL이 5분인 6자리 난수 토큰 정보를 생성하고, 이를 userId + 토큰 정보를 key로, userId정보를 value로 반환하고 해당 값을 토대로 경계선 지능인을 센터에 등록함으로서 검증을 진행하게 된다.

 

 

service.RelationshipService.java

package com.a102.relation.service;

import com.a102.relation.dto.response.*;
import com.a102.relation.dto.request.*;
import com.a102.relation.entity.*;
import com.a102.relation.exception.RelationshipAlreadyExistsException;
import com.a102.relation.exception.ResourceNotFoundException;
import com.a102.relation.repository.*;
import com.a102.relation.util.UserContextUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class RelationshipService {

    private final UserRepository userRepository;
    private final BIFriendRepository biFriendRepository;
    private final ParentChildRepository parentChildRepository;
    private final CenterBIRepository centerBIRepository;
    private final UserContextUtil userContextUtil;
    private final BIUserRepository biUserRepository;
    private final CenterRepository centerRepository;

    /**
     * 모든 관계 정보를 조회합니다.
     */
    @Transactional(readOnly = true)
    public List<RelationshipResponse> getAllRelationships() {
        List<RelationshipResponse> result = new ArrayList<>();

        // BIFriend 관계 조회
        List<BiFriend> biFriends = biFriendRepository.findAll();
        for (BiFriend biFriend : biFriends) {
            result.add(RelationshipResponse.fromBIFriend(biFriend, biFriend.getBiUserId1()));
        }

        // ParentChild 관계 조회
        List<ParentChild> parentChildren = parentChildRepository.findAll();
        for (ParentChild parentChild : parentChildren) {
            result.add(RelationshipResponse.fromParentChild(parentChild, true));
        }

        // CenterBi 관계 조회
        List<CenterBi> centerBIs = centerBIRepository.findAll();
        for (CenterBi CenterBi : centerBIs) {
            result.add(RelationshipResponse.fromCenterBI(CenterBi, true));
        }

        return result;
    }

    @Transactional(readOnly = true)
    public List<CenterInfoResponse> getCentersByBIUser(String biUserId) {
        // BiUser가 존재하는지 확인
        Optional<BiUser> biUserOptional = biUserRepository.findById(biUserId);
        if (biUserOptional.isEmpty()) {
            throw new IllegalArgumentException("해당 경계선 지능인을 찾을 수 없습니다.");
        }

        // CenterBi 테이블에서 경계선 지능인이 소속된 센터 정보 조회
        List<CenterBi> centerBiList = centerBIRepository.findByBiUserId(biUserId);
        List<CenterInfoResponse> centerInfoList = new ArrayList<>();

        for (CenterBi centerBi : centerBiList) {
            String centerUserId = centerBi.getCenterUserId();

            // Center 정보 조회
            Optional<Center> centerOptional = centerRepository.findById(centerUserId);
            if (centerOptional.isEmpty()) {
                continue;
            }
            Center center = centerOptional.get();

            // User 정보 조회
            Optional<User> userOptional = userRepository.findById(centerUserId);
            if (userOptional.isEmpty()) {
                continue;
            }
            User user = userOptional.get();

            // DTO로 변환하여 리스트에 추가
            CenterInfoResponse centerInfo = CenterInfoResponse.fromEntity(
                    center, user, centerBi.getStatus(), centerBi.getStartDate(), centerBi.getEndDate()
            );
            centerInfoList.add(centerInfo);
        }

        return centerInfoList;
    }

    /**
     * 특정 사용자의 모든 관계 정보를 조회합니다.
     */
    @Transactional(readOnly = true)
    public List<RelationshipResponse> getRelationshipsByUserId(String userId) {
        // API 게이트웨이에서 받은 사용자 ID 검증
        String validatedUserId = userContextUtil.validateAndGetUserId(userId);

        User user = userRepository.findById(validatedUserId)
                .orElseThrow(() -> new ResourceNotFoundException("User", "id", validatedUserId));

        List<RelationshipResponse> result = new ArrayList<>();

        // 사용자 유형에 따라 조회 로직 분기
        switch (user.getType()) {
            case "bid":
                // BIFriend 관계 조회 (양방향)
                List<BiFriend> biFriends1 = biFriendRepository.findByBiUserId1(validatedUserId);
                List<BiFriend> biFriends2 = biFriendRepository.findByBiUserId2(validatedUserId);

                for (BiFriend biFriend : biFriends1) {
                    result.add(RelationshipResponse.fromBIFriend(biFriend, validatedUserId));
                }
                for (BiFriend biFriend : biFriends2) {
                    result.add(RelationshipResponse.fromBIFriend(biFriend, validatedUserId));
                }

                // ParentChild 관계 조회 (자녀 입장)
                List<ParentChild> childRelations = parentChildRepository.findByChildUserId(validatedUserId);
                for (ParentChild parentChild : childRelations) {
                    result.add(RelationshipResponse.fromParentChild(parentChild, false));
                }

                // CenterBi 관계 조회 (BI 입장)
                List<CenterBi> biCenterRelations = centerBIRepository.findByBiUserId(validatedUserId);
                for (CenterBi CenterBi : biCenterRelations) {
                    result.add(RelationshipResponse.fromCenterBI(CenterBi, false));
                }
                break;

            case "parent":
                // ParentChild 관계 조회 (부모 입장)
                List<ParentChild> parentRelations = parentChildRepository.findByParentUserId(validatedUserId);
                for (ParentChild parentChild : parentRelations) {
                    result.add(RelationshipResponse.fromParentChild(parentChild, true));
                }
                break;

            case "center":
                // CenterBi 관계 조회 (센터 입장)
                List<CenterBi> centerRelations = centerBIRepository.findByCenterUserId(validatedUserId);
                for (CenterBi CenterBi : centerRelations) {
                    result.add(RelationshipResponse.fromCenterBI(CenterBi, true));
                }
                break;
        }

        return result;
    }

    /**
     * 특정 유형의 관계만 조회합니다.
     * @param userId 사용자 ID
     * @param type 관계 유형 ("friend", "parent-child", "center-bi")
     */
    @Transactional(readOnly = true)
    public List<RelationshipResponse> getRelationshipsByType(String userId, String type) {
        // API 게이트웨이에서 받은 사용자 ID 검증
        String validatedUserId = userContextUtil.validateAndGetUserId(userId);

        List<RelationshipResponse> allRelationships = getRelationshipsByUserId(validatedUserId);

        return allRelationships.stream()
                .filter(relationship -> relationship.getRelationshipType().equals(type))
                .collect(Collectors.toList());
    }

    /**
     * 새로운 관계를 생성합니다.
     */
    @Transactional
    public RelationshipResponse createRelationship(String sourceUserId, String targetUserId, RelationshipRequest request) {
        // API 게이트웨이에서 받은 사용자 ID 검증
        String validatedSourceUserId = userContextUtil.validateAndGetUserId(sourceUserId);

        if (targetUserId == null || targetUserId.isEmpty()) {
            throw new IllegalArgumentException("Target user ID cannot be null or empty");
        }

        User sourceUser = userRepository.findById(validatedSourceUserId)
                .orElseThrow(() -> new ResourceNotFoundException("Source user", "id", validatedSourceUserId));

        User targetUser = userRepository.findById(targetUserId)
                .orElseThrow(() -> new ResourceNotFoundException("Target user", "id", targetUserId));

        String type = request.getType();
        if (type == null || type.isEmpty()) {
            throw new IllegalArgumentException("Relationship type cannot be null or empty");
        }

        switch (type) {
            case "friend":
                return createFriendRelationship(sourceUser, targetUser, request);

            case "parent-child":
                return createParentChildRelationship(sourceUser, targetUser, request);

            case "center-bi":
                return createCenterBIRelationship(sourceUser, targetUser, request);

            default:
                throw new IllegalArgumentException("Unsupported relationship type: " + type);
        }
    }

    /**
     * 친구 관계를 생성합니다.
     */
    private RelationshipResponse createFriendRelationship(User sourceUser, User targetUser, RelationshipRequest request) {
        // sourceUser와 targetUser 모두 BIUser 타입인지 확인
        if (!"bid".equals(sourceUser.getType()) || !"bid".equals(targetUser.getType())) {
            throw new IllegalArgumentException("Friend relationship can only be created between two BIUsers");
        }

        // 이미 친구 관계가 있는지 확인
        boolean existingRelationship = biFriendRepository.existsByBiUserId1AndBiUserId2(
                sourceUser.getId(), targetUser.getId()) ||
                biFriendRepository.existsByBiUserId1AndBiUserId2(
                        targetUser.getId(), sourceUser.getId());

        if (existingRelationship) {
            throw new RelationshipAlreadyExistsException("Friend", sourceUser.getId(), targetUser.getId());
        }

        String relationshipStatus = "pending";  // Enum -> String
        if (request.getStatus() != null && !request.getStatus().isEmpty()) {
            relationshipStatus = request.getStatus();  // Enum.valueOf() -> 직접 할당
        }

        BiFriend biFriend = BiFriend.builder()
                .id(UUID.randomUUID().toString())
                .biUserId1(sourceUser.getId())
                .biUserId2(targetUser.getId())
                .relationshipStatus(relationshipStatus)  // Enum -> String
                .status("active")  // Enum -> String
                .build();

        BiFriend savedBiFriend = biFriendRepository.save(biFriend);
        return RelationshipResponse.fromBIFriend(savedBiFriend, sourceUser.getId());
    }

    /**
     * 부모-자녀 관계를 생성합니다.
     */
    private RelationshipResponse createParentChildRelationship(User sourceUser, User targetUser, RelationshipRequest request) {
        // sourceUser는 parent 타입, targetUser는 bid 타입인지 확인
        if (!"parent".equals(sourceUser.getType()) || !"bid".equals(targetUser.getType())) {
            // 순서가 반대인 경우도 처리 (Enum -> String)
            if ("parent".equals(targetUser.getType()) && "bid".equals(sourceUser.getType())) {
                return createParentChildRelationship(targetUser, sourceUser, request);
            }
            throw new IllegalArgumentException("Parent-child relationship requires a parent and a BIUser");
        }

        // 이미 부모-자녀 관계가 있는지 확인
        boolean existingRelationship = parentChildRepository.existsByParentUserIdAndChildUserId(
                sourceUser.getId(), targetUser.getId());

        if (existingRelationship) {
            throw new RelationshipAlreadyExistsException("Parent-child", sourceUser.getId(), targetUser.getId());
        }

        // 자녀에게 이미 두 명의 부모가 있는지 확인
        long parentCount = parentChildRepository.countByChildUserId(targetUser.getId());
        if (parentCount >= 2) {
            throw new IllegalArgumentException("A child cannot have more than 2 parents");
        }

        String status = "active";  // Enum -> String
        if (request.getStatus() != null && !request.getStatus().isEmpty()) {
            status = request.getStatus();  // Enum.valueOf() -> 직접 할당
        }

        ParentChild parentChild = ParentChild.builder()
                .id(UUID.randomUUID().toString())
                .parentUserId(sourceUser.getId())
                .childUserId(targetUser.getId())
                .relationshipType(request.getRelationType())
                .status(status)  // Enum -> String
                .build();

        ParentChild savedParentChild = parentChildRepository.save(parentChild);
        return RelationshipResponse.fromParentChild(savedParentChild, true);
    }

    /**
     * 센터-경계선 지능인 관계를 생성합니다.
     */
    private RelationshipResponse createCenterBIRelationship(User sourceUser, User targetUser, RelationshipRequest request) {
        // sourceUser는 center 타입, targetUser는 bid 타입인지 확인
        if (!"center".equals(sourceUser.getType()) || !"bid".equals(targetUser.getType())) {
            // 순서가 반대인 경우도 처리 (Enum -> String)
            if ("center".equals(targetUser.getType()) && "bid".equals(sourceUser.getType())) {
                return createCenterBIRelationship(targetUser, sourceUser, request);
            }
            throw new IllegalArgumentException("Center-BI relationship requires a center and a BIUser");
        }

        // 이미 센터-경계선 지능인 관계가 있는지 확인
        boolean existingRelationship = centerBIRepository.existsByCenterUserIdAndBiUserId(
                sourceUser.getId(), targetUser.getId());

        if (existingRelationship) {
            throw new RelationshipAlreadyExistsException("Center-BI", sourceUser.getId(), targetUser.getId());
        }

        String status = "active";  // Enum -> String
        if (request.getStatus() != null && !request.getStatus().isEmpty()) {
            status = request.getStatus();  // Enum.valueOf() -> 직접 할당
        }

        CenterBi centerBi = CenterBi.builder()
                .id(UUID.randomUUID().toString())
                .centerUserId(sourceUser.getId())
                .biUserId(targetUser.getId())
                .startDate(LocalDate.now())
                .status(status)  // Enum -> String
                .build();

        CenterBi savedCenterBI = centerBIRepository.save(centerBi);
        return RelationshipResponse.fromCenterBI(savedCenterBI, true);
    }

    /**
     * 관계를 삭제합니다.
     */
    @Transactional
    public void deleteRelationship(String sourceUserId, String targetUserId) {
        // API 게이트웨이에서 받은 사용자 ID 검증
        String validatedSourceUserId = userContextUtil.validateAndGetUserId(sourceUserId);

        if (targetUserId == null || targetUserId.isEmpty()) {
            throw new IllegalArgumentException("Target user ID cannot be null or empty");
        }

        User sourceUser = userRepository.findById(validatedSourceUserId)
                .orElseThrow(() -> new ResourceNotFoundException("Source user", "id", validatedSourceUserId));

        User targetUser = userRepository.findById(targetUserId)
                .orElseThrow(() -> new ResourceNotFoundException("Target user", "id", targetUserId));

        // 사용자 타입에 따라 삭제할 관계 결정 (Enum -> String)
        switch (sourceUser.getType()) {
            case "bid":  // Enum -> String
                if ("bid".equals(targetUser.getType())) {  // Enum -> String
                    // BI-BI 친구 관계 삭제
                    BiFriend biFriend = biFriendRepository.findByBiUserId1AndBiUserId2(validatedSourceUserId, targetUserId)
                            .orElse(biFriendRepository.findByBiUserId1AndBiUserId2(targetUserId, validatedSourceUserId)
                                    .orElse(null));

                    if (biFriend != null) {
                        biFriendRepository.delete(biFriend);
                        return;
                    }
                } else if ("parent".equals(targetUser.getType())) {  // Enum -> String
                    // BI-Parent 관계 삭제 (자녀 입장)
                    ParentChild parentChild = parentChildRepository.findByParentUserIdAndChildUserId(targetUserId, validatedSourceUserId)
                            .orElse(null);

                    if (parentChild != null) {
                        parentChildRepository.delete(parentChild);
                        return;
                    }
                } else if ("center".equals(targetUser.getType())) {  // Enum -> String
                    // BI-Center 관계 삭제 (BI 입장)
                    CenterBi CenterBi = centerBIRepository.findByCenterUserIdAndBiUserId(targetUserId, validatedSourceUserId)
                            .orElse(null);

                    if (CenterBi != null) {
                        centerBIRepository.delete(CenterBi);
                        return;
                    }
                }
                break;

            case "parent":  // Enum -> String
                if ("bid".equals(targetUser.getType())) {  // Enum -> String
                    // Parent-BI 관계 삭제 (부모 입장)
                    ParentChild parentChild = parentChildRepository.findByParentUserIdAndChildUserId(validatedSourceUserId, targetUserId)
                            .orElse(null);

                    if (parentChild != null) {
                        parentChildRepository.delete(parentChild);
                        return;
                    }
                }
                break;

            case "center":  // Enum -> String
                if ("bid".equals(targetUser.getType())) {  // Enum -> String
                    // Center-BI 관계 삭제 (센터 입장)
                    CenterBi CenterBi = centerBIRepository.findByCenterUserIdAndBiUserId(validatedSourceUserId, targetUserId)
                            .orElse(null);

                    if (CenterBi != null) {
                        centerBIRepository.delete(CenterBi);
                        return;
                    }
                }
                break;
        }

        throw new ResourceNotFoundException("Relationship", "between users", validatedSourceUserId + " and " + targetUserId);
    }
}

 

경계선 지능인의 입장에서 관계와 관련된 CRUD 트랜잭션을 수행하는 비즈니스 로직이다. 특이 사항으로 관리자의 관점에서 현재 DB에 존재하는 모든 관계 정보를 조회하는 기능이 존재한다.

 

 

service.StoreWorkerService.java

package com.a102.relation.service;

import com.a102.relation.dto.request.*;
import com.a102.relation.dto.response.*;
import com.a102.relation.entity.*;
import com.a102.relation.exception.EntityNotFoundException;
import com.a102.relation.exception.ResourceNotFoundException;
import com.a102.relation.exception.RelationshipAlreadyExistsException;
import com.a102.relation.repository.*;
import com.a102.relation.util.UserContextUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class StoreWorkerService {

    private final UserRepository userRepository;
    private final BIUserRepository biUserRepository;
    private final BIStoreRepository biStoreRepository;
    private final StoreRepository storeRepository;
    private final UserContextUtil userContextUtil;

    /**
     * 경계선 지능인이 일하는 모든 상점을 조회합니다.
     */
    @Transactional(readOnly = true)
    public List<StoreWorkerResponse> getStoresByWorker(String biUserId) {
        // API 게이트웨이에서 받은 사용자 ID 검증
        String validatedUserId = userContextUtil.validateAndGetUserId(biUserId);

        // 경계선 지능인인지 확인
        User biUser = userRepository.findById(validatedUserId)
                .orElseThrow(() -> new ResourceNotFoundException("BI User", "id", validatedUserId));

        if (!biUser.getType().equals("bid")) {
            throw new IllegalArgumentException("User is not a BI type");
        }

        // 경계선 지능인이 일하는 모든 상점 조회
        List<BiStore> biStores = biStoreRepository.findByBiUserId(validatedUserId);

        // 응답 객체로 변환
        return biStores.stream()
                .map(StoreWorkerResponse::fromEntity)
                .collect(Collectors.toList());
    }

    /**
     * 모든 상점 근무자를 조회합니다.
     */
    @Transactional(readOnly = true)
    public List<StoreWorkerResponse> getAllWorkers(String centerUserId) {
        // API 게이트웨이에서 받은 사용자 ID 검증
        String validatedUserId = userContextUtil.validateAndGetUserId(centerUserId);

        // 센터 사용자인지 확인
        User centerUser = userRepository.findById(validatedUserId)
                .orElseThrow(() -> new ResourceNotFoundException("Center User", "id", validatedUserId));

        if (!centerUser.getType().equals("center")) {
            throw new IllegalArgumentException("User is not a center type");
        }

        // 모든 상점 근무자 조회 (센터가 관리하는 상점들에 대해서만)
        List<BiStore> allWorkers = new ArrayList<>();

        // 서비스 연결 로직 필요 (Center -> CenterStore -> Store -> BIStore)
        // 실제 구현에서는 복잡한 조인 쿼리나 여러 엔티티 조회로 처리

        // 임시 구현: 모든 근무자 조회
        allWorkers = biStoreRepository.findAll();

        return allWorkers.stream()
                .map(StoreWorkerResponse::fromEntity)
                .collect(Collectors.toList());
    }

    /**
     * 특정 상점의 모든 근무자를 조회합니다.
     */
    @Transactional(readOnly = true)
    public List<StoreWorkerResponse> getWorkersByStore(String storeId) {
        if (storeId == null || storeId.isEmpty()) {
            throw new IllegalArgumentException("Store ID cannot be null or empty");
        }

        // 상점이 존재하는지 확인
        Store store = storeRepository.findById(storeId)
                .orElseThrow(() -> new ResourceNotFoundException("Store", "id", storeId));

        // 상점의 모든 근무자 조회
        List<BiStore> storeWorkers = biStoreRepository.findByStoreId(storeId);

        return storeWorkers.stream()
                .map(StoreWorkerResponse::fromEntity)
                .collect(Collectors.toList());
    }

    /**
     * 특정 상점의 특정 근무자를 조회합니다.
     */
    @Transactional(readOnly = true)
    public StoreWorkerResponse getWorkerById(String storeId, String biUserId) {
        if (storeId == null || storeId.isEmpty()) {
            throw new IllegalArgumentException("Store ID cannot be null or empty");
        }
        if (biUserId == null || biUserId.isEmpty()) {
            throw new IllegalArgumentException("BI User ID cannot be null or empty");
        }

        // 근무자 관계 조회
        BiStore biStore = biStoreRepository.findByStoreIdAndBiUserId(storeId, biUserId)
                .orElseThrow(() -> new ResourceNotFoundException("Worker relationship", "between store and BI user",
                        storeId + " and " + biUserId));

        return StoreWorkerResponse.fromEntity(biStore);
    }

    @Transactional
    public StoreWorkerResponse createWorker(String storeId, StoreWorkerRequest request) {
        if (storeId == null || storeId.isEmpty()) {
            throw new IllegalArgumentException("Store ID cannot be null or empty");
        }
        if (request.getBiUserId() == null || request.getBiUserId().isEmpty()) {
            throw new IllegalArgumentException("BI User ID cannot be null or empty");
        }

        // 상점이 존재하는지 확인
        Store store = storeRepository.findById(storeId)
                .orElseThrow(() -> new ResourceNotFoundException("Store", "id", storeId));

        // 경계선 지능인이 존재하는지 확인
        User biUser = userRepository.findById(request.getBiUserId())
                .orElseThrow(() -> new ResourceNotFoundException("BI User", "id", request.getBiUserId()));

        if (!biUser.getType().equals("bid")) {
            throw new IllegalArgumentException("User is not a BI type");
        }

        // 이미 관계가 있는지 확인
        boolean existingRelationship = biStoreRepository.existsByStoreIdAndBiUserId(storeId, request.getBiUserId());
        if (existingRelationship) {
            throw new RelationshipAlreadyExistsException("Worker", storeId, request.getBiUserId());
        }

        // 상태 값 처리
        String status = "active";  // Enum -> String
        if (request.getStatus() != null && !request.getStatus().isEmpty()) {
            status = request.getStatus();  // Enum.valueOf() -> 직접 할당
        }

        // BiUser 엔티티 로드
        BiUser biUserEntity = biUserRepository.findById(request.getBiUserId())
                .orElseThrow(() -> new ResourceNotFoundException("BI User", "id", request.getBiUserId()));

        // Store 엔티티도 직접 설정해야 함
        BiStore biStore = BiStore.builder()
                .id(UUID.randomUUID().toString())
                .storeId(storeId)
                .biUserId(request.getBiUserId())
                .role(request.getRole())
                .startDate(request.getStartDate())
                .endDate(request.getEndDate())
                .status(status)  // Enum -> String
                .build();

        // 연관 객체 직접 설정
        biStore.setBiUser(biUserEntity);
        biStore.setStore(store); // Store도 연관 객체로 설정

        BiStore savedBiStore = biStoreRepository.save(biStore);
        return StoreWorkerResponse.fromEntity(savedBiStore);
    }

    /**
     * 근무자 정보를 업데이트합니다.
     */
    @Transactional
    public StoreWorkerResponse updateWorker(String storeId, StoreWorkerRequest request) {
        if (storeId == null || storeId.isEmpty()) {
            throw new IllegalArgumentException("Store ID cannot be null or empty");
        }
        if (request.getBiUserId() == null || request.getBiUserId().isEmpty()) {
            throw new IllegalArgumentException("BI User ID cannot be null or empty");
        }

        // 근무자 관계 조회
        BiStore biStore = biStoreRepository.findByStoreIdAndBiUserId(storeId, request.getBiUserId())
                .orElseThrow(() -> new ResourceNotFoundException("Worker relationship", "between store and BI user",
                        storeId + " and " + request.getBiUserId()));

        // 상태 값 처리
        if (request.getStatus() != null && !request.getStatus().isEmpty()) {
            biStore.setStatus(request.getStatus());  // Enum.valueOf() -> 직접 할당
        }

        // 정보 업데이트
        biStore.setRole(request.getRole());
        biStore.setStartDate(request.getStartDate());
        biStore.setEndDate(request.getEndDate());

        BiStore updatedBiStore = biStoreRepository.save(biStore);

        return StoreWorkerResponse.fromEntity(updatedBiStore);
    }

    /**
     * 상점의 모든 근무자를 삭제합니다.
     */
    @Transactional
    public void deleteWorker(String storeId) {
        if (storeId == null || storeId.isEmpty()) {
            throw new IllegalArgumentException("Store ID cannot be null or empty");
        }

        // 상점이 존재하는지 확인
        Store store = storeRepository.findById(storeId)
                .orElseThrow(() -> new ResourceNotFoundException("Store", "id", storeId));

        // 상점의 모든 근무자 관계 삭제
        List<BiStore> workers = biStoreRepository.findByStoreId(storeId);
        biStoreRepository.deleteAll(workers);
    }

    /**
     * 특정 상점의 특정 근무자를 삭제합니다.
     */
    @Transactional
    public void deleteWorkerByStoreAndUser(String storeId, String biUserId) {
        if (storeId == null || storeId.isEmpty()) {
            throw new IllegalArgumentException("Store ID cannot be null or empty");
        }
        if (biUserId == null || biUserId.isEmpty()) {
            throw new IllegalArgumentException("BI User ID cannot be null or empty");
        }

        // 근무자 관계 조회
        BiStore biStore = biStoreRepository.findByStoreIdAndBiUserId(storeId, biUserId)
                .orElseThrow(() -> new ResourceNotFoundException("Worker relationship", "between store and BI user",
                        storeId + " and " + biUserId));

        // 관계 삭제
        biStoreRepository.delete(biStore);
    }
}

 

센터의 관점에서 가게와 경계선 지능인의 관계를 설정하는 CRUD 트랜잭션을 수행하는 비즈니스 로직이다.

 

 

service.S3Service.java

package com.a102.relation.service;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.Bucket;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;
import java.util.UUID;

@Slf4j
@Service
public class S3Service {

    private final AmazonS3 s3Client;
    private final String bucketName;

    public S3Service(AmazonS3 s3Client, @Value("${cloud.aws.s3.bucket}") String bucketName) {
        this.s3Client = s3Client;
        this.bucketName = bucketName;
        log.info("S3Service initialized with bucket: {}", bucketName);
    }

    @PostConstruct
    public void init() {
        try {
            log.info("Testing S3 connection...");

            if (s3Client == null) {
                log.error("S3 client is null! Bean injection failed.");
                return;
            }

            List<Bucket> buckets = s3Client.listBuckets();
            log.info("S3 connection successful! Found {} buckets", buckets.size());

            boolean bucketExists = s3Client.doesBucketExistV2(bucketName);
            log.info("Bucket '{}' exists: {}", bucketName, bucketExists);

            if (!bucketExists) {
                log.warn("The specified bucket does not exist or you don't have access to it");
            }
        } catch (Exception e) {
            log.error("Failed to connect to AWS S3: {}", e.getMessage(), e);
        }
    }

    /**
     * 프로필 이미지를 S3에 업로드하고 URL을 반환
     */
    public String uploadImage(MultipartFile file, String Id) {
        if (file == null || file.isEmpty()) {
            log.warn("Empty file provided for user: {}", Id);
            return null;
        }

        try {
            log.info("Uploading profile image for user: {}", Id);
            log.info("File details - Name: {}, Size: {}, Type: {}",
                    file.getOriginalFilename(), file.getSize(), file.getContentType());

            // 파일 확장자 추출
            String originalFilename = file.getOriginalFilename();
            String ext = originalFilename != null
                    ? originalFilename.substring(originalFilename.lastIndexOf("."))
                    : "";

            // 고유한 파일명 생성 (경로 포함)
            String storeFileName = "profiles/" + Id + "/" + UUID.randomUUID() + ext;
            log.info("Generated file path: {}", storeFileName);

            // 메타데이터 설정
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentType(file.getContentType());
            metadata.setContentLength(file.getSize());

            // S3에 업로드
            log.info("Uploading to S3 - Bucket: {}, File path: {}", bucketName, storeFileName);
            s3Client.putObject(
                    new PutObjectRequest(bucketName, storeFileName, file.getInputStream(), metadata)
            );

            // S3 URL 반환
            String fileUrl = s3Client.getUrl(bucketName, storeFileName).toString();
            log.info("Upload successful, URL: {}", fileUrl);
            return fileUrl;

        } catch (IOException e) {
            log.error("IO Error uploading profile image for user: {}", Id, e);
            throw new RuntimeException("Failed to upload profile image - IO Error", e);
        } catch (Exception e) {
            log.error("Unexpected error uploading profile image for user: {}", Id, e);
            throw new RuntimeException("Failed to upload profile image - " + e.getMessage(), e);
        }
    }
}

 

센터에서 가게 등록 시 가게 이미지 파일 처리를 위한 AWS 서비스 로직이다.

 

 

service.TokenService.java

package com.a102.relation.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class TokenService {

    private final StringRedisTemplate redisTemplate;

    @Autowired
    public TokenService(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public String validateToken(String token) {
        String userId = redisTemplate.opsForValue().get("user_token:" + token);
        return userId.isEmpty() ? null : userId;
    }
}

 

센터에서 경계선 지능인 추가 시 Redis에서 사용자 토큰 정보를 검증하기 위한 서비스 로직이다.

 

 

util.UserContextUtil.java

package com.a102.relation.util;

import com.a102.relation.exception.InvalidTokenException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * API 게이트웨이로부터 전달받은 사용자 정보를 처리하는 유틸리티 클래스
 */
@Component
public class UserContextUtil {

    /**
     * 요청 헤더에서 전달받은 사용자 ID를 검증합니다.
     * API 게이트웨이에서 인증이 완료된 사용자 ID(X-User-ID)를 처리합니다.
     *
     * @param userId 컨트롤러에서 @RequestHeader로 받은 X-User-ID 값
     * @return 검증된's 사용자 ID
     * @throws InvalidTokenException 사용자 ID가 없거나 유효하지 않은 경우
     */
    public String validateAndGetUserId(String userId) {
        if (!StringUtils.hasText(userId)) {
            throw new InvalidTokenException("Missing user ID in request header (X-User-ID)");
        }

        // 여기에 필요한 추가 검증 로직을 구현할 수 있습니다.
        // 예: userId 형식 검증, 특수 케이스 처리 등

        return userId;
    }
}

 

요청 헤더에서 전달받은 사용자 ID를 검정하는 로직이다.

 

 

RelationServiceApplication

// RelationServiceApplication.java
package com.a102.relation;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RelationServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(RelationServiceApplication.class, args);
    }
}

서비스의 진입점이다.



application.yml

server:
  port: 8082

spring:
  application:
    name: relation-service

  servlet:
    multipart:
      enabled: true  # 기본값은 true이므로 생략 가능
      max-file-size: 10MB  # 필요에 따라 조정
      max-request-size: 10MB  # 필요에 따라 조정

  # MariaDB 설정 - 환경 변수 사용
  datasource:
    url: "${DB_URL}"
    username: "${DB_USERNAME}"
    password: "${DB_PASSWORD}"
    driver-class-name: org.mariadb.jdbc.Driver
    hikari:
      maximum-pool-size: 10  # 최대 연결 풀 크기 (서버 상황에 맞게 조정)
      minimum-idle: 7  # 최소 유휴 연결 수
      idle-timeout: 30000  # 연결 유휴 상태 30초 후 풀에서 제거
      connection-timeout: 30000  # 30초 내에 연결 얻지 못하면 예외 발생
      max-lifetime: 1800000  # 연결의 최대 수명 (30분)
      pool-name: RelationHikariCP

  jpa:
    hibernate:
      ddl-auto: none
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.MariaDBDialect

  data:
    redis:
      host: redis-server
      port: 6379

cloud:
  aws:
    credentials:
      access-key: "${AWS_ACCESS_KEY_ID}"
      secret-key: "${AWS_SECRET_ACCESS_KEY}"
    region:
      static: "${AWS_REGION}"
    stack:
      auto: false  # CloudFormation 스택 자동 감지 비활성화
    s3:
      bucket: "${S3_BUCKET_NAME}"

springdoc:
  api-docs:
    path: /v3/api-docs
  swagger-ui:
    path: /swagger-ui.html
  server-url: "${API_GATEWAY_URL}"

logging:
  level:
    com.a102.relation: DEBUG
    org.springframework.web: INFO
    org.hibernate: ERROR
    com.zaxxer.hikari: INFO
    com.zaxxer.hikari.HikariConfig: INFO

 

서비스에 대한 정의와 MariaDB, JPA, Redis, AWS S3, Swagger와 로깅 관련 설정을 위한 파일이다. 민감한 정보는 환경 변수를 전달받아 관리된다.

 

 

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.2</version>
        <relativePath/>
    </parent>

    <groupId>com.a102</groupId>
    <artifactId>relation-service</artifactId>
    <version>1.0.0</version>
    <name>auth-service</name>
    <description>Relation Service for A102 Project</description>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <!-- Spring Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- AWS S3 -->
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-s3</artifactId>
            <version>1.12.595</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-aws</artifactId>
            <version>2.2.6.RELEASE</version>
        </dependency>

        <!-- Spring Data JPA -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <!-- MariaDB Driver -->
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>

        <!-- Validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- SpringDoc OpenAPI (for Swagger) -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.3.0</version>
        </dependency>

        <!-- Spring Data Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

 

서비스에 필요한 mvn빌드 의존성 관련 파일이다. SpringBoot 3.2.2버전에 맞게 세팅되어 있다.

728x90