From aa3f93d94913af4d1ecbab4aed2733da0bb9205d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Mon, 3 Mar 2025 21:22:07 +0900
Subject: [PATCH 01/41] =?UTF-8?q?Feat/auth=20:=20=EC=82=AC=EC=9A=A9?=
 =?UTF-8?q?=EC=9E=90=20=ED=86=A0=ED=81=B0=20=EB=B0=9C=ED=96=89=20=EB=B0=8F?=
 =?UTF-8?q?=20=EC=B0=B8=EC=97=AC=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?=
 =?UTF-8?q?=EB=B0=98=ED=99=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 build.gradle                                  |   2 +
 .../java/com/aolda/itda/config/WebConfig.java |  19 +++
 .../aolda/itda/controller/AuthController.java |  27 +++
 .../aolda/itda/dto/auth/LoginRequestDTO.java  |  11 ++
 .../aolda/itda/dto/auth/LoginResponseDTO.java |  17 ++
 .../itda/dto/auth/ProjectIdAndNameDTO.java    |  16 ++
 .../aolda/itda/dto/auth/ProjectRoleDTO.java   |  17 ++
 .../itda/exception/ApiExceptionHandler.java   |  17 ++
 .../aolda/itda/exception/CustomException.java |  19 +++
 .../com/aolda/itda/exception/ErrorCode.java   |  16 ++
 .../aolda/itda/exception/ErrorResponse.java   |  29 ++++
 .../com/aolda/itda/service/AuthService.java   | 159 ++++++++++++++++++
 12 files changed, 349 insertions(+)
 create mode 100644 src/main/java/com/aolda/itda/config/WebConfig.java
 create mode 100644 src/main/java/com/aolda/itda/controller/AuthController.java
 create mode 100644 src/main/java/com/aolda/itda/dto/auth/LoginRequestDTO.java
 create mode 100644 src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java
 create mode 100644 src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java
 create mode 100644 src/main/java/com/aolda/itda/dto/auth/ProjectRoleDTO.java
 create mode 100644 src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java
 create mode 100644 src/main/java/com/aolda/itda/exception/CustomException.java
 create mode 100644 src/main/java/com/aolda/itda/exception/ErrorCode.java
 create mode 100644 src/main/java/com/aolda/itda/exception/ErrorResponse.java
 create mode 100644 src/main/java/com/aolda/itda/service/AuthService.java

diff --git a/build.gradle b/build.gradle
index 9305e70..5cef9f6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -28,6 +28,8 @@ dependencies {
     implementation 'org.springframework.boot:spring-boot-starter-web'
     compileOnly 'org.projectlombok:lombok'
     runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
+    runtimeOnly 'com.mysql:mysql-connector-j'
+    implementation 'org.pacesys:openstack4j:3.1.0'
     annotationProcessor 'org.projectlombok:lombok'
     testImplementation 'org.springframework.boot:spring-boot-starter-test'
     testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
diff --git a/src/main/java/com/aolda/itda/config/WebConfig.java b/src/main/java/com/aolda/itda/config/WebConfig.java
new file mode 100644
index 0000000..9eb992b
--- /dev/null
+++ b/src/main/java/com/aolda/itda/config/WebConfig.java
@@ -0,0 +1,19 @@
+package com.aolda.itda.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+    @Override
+    public void addCorsMappings(CorsRegistry registry) { // 스프링단에서 cors 설정
+        registry.addMapping("/**")
+                .allowedOriginPatterns("*")
+                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "FETCH")
+                .allowedHeaders("*")
+                .allowCredentials(true)
+                .exposedHeaders("Authorization", "X-Refresh-Token", "Access-Control-Allow-Origin")
+        ;
+    }
+}
diff --git a/src/main/java/com/aolda/itda/controller/AuthController.java b/src/main/java/com/aolda/itda/controller/AuthController.java
new file mode 100644
index 0000000..274ad8f
--- /dev/null
+++ b/src/main/java/com/aolda/itda/controller/AuthController.java
@@ -0,0 +1,27 @@
+package com.aolda.itda.controller;
+
+import com.aolda.itda.dto.auth.LoginRequestDTO;
+import com.aolda.itda.service.AuthService;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/auth")
+@RequiredArgsConstructor
+public class AuthController {
+
+    private final AuthService authService;
+
+    @PostMapping("/login")
+    public ResponseEntity<Object> login(HttpServletResponse response,
+                                        @RequestBody LoginRequestDTO loginRequestDTO) throws JsonProcessingException {
+
+        return ResponseEntity.ok(authService.userLogin(response, loginRequestDTO));
+    }
+}
diff --git a/src/main/java/com/aolda/itda/dto/auth/LoginRequestDTO.java b/src/main/java/com/aolda/itda/dto/auth/LoginRequestDTO.java
new file mode 100644
index 0000000..b91b3af
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/auth/LoginRequestDTO.java
@@ -0,0 +1,11 @@
+package com.aolda.itda.dto.auth;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+public class LoginRequestDTO {
+    private String id;
+    private String password;
+}
diff --git a/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java b/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java
new file mode 100644
index 0000000..a5aa14a
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java
@@ -0,0 +1,17 @@
+package com.aolda.itda.dto.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class LoginResponseDTO {
+    private Boolean isAdmin;
+    private List<ProjectIdAndNameDTO> lists;
+}
diff --git a/src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java b/src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java
new file mode 100644
index 0000000..1ca894d
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java
@@ -0,0 +1,16 @@
+package com.aolda.itda.dto.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class ProjectIdAndNameDTO {
+
+    private String id;
+    private String name;
+}
diff --git a/src/main/java/com/aolda/itda/dto/auth/ProjectRoleDTO.java b/src/main/java/com/aolda/itda/dto/auth/ProjectRoleDTO.java
new file mode 100644
index 0000000..d3b9b8e
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/auth/ProjectRoleDTO.java
@@ -0,0 +1,17 @@
+package com.aolda.itda.dto.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class ProjectRoleDTO {
+    private String projectName;
+    private String roleName;
+}
diff --git a/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java b/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java
new file mode 100644
index 0000000..4d1f360
--- /dev/null
+++ b/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java
@@ -0,0 +1,17 @@
+package com.aolda.itda.exception;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@Slf4j
+@RestControllerAdvice
+public class ApiExceptionHandler {
+
+    @ExceptionHandler(value = CustomException.class)
+    public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
+        log.error("[handleCustomException] {} : {}", e.getErrorCode().name(), e.getErrorCode().getMessage());
+        return ErrorResponse.fromException(e);
+    }
+}
diff --git a/src/main/java/com/aolda/itda/exception/CustomException.java b/src/main/java/com/aolda/itda/exception/CustomException.java
new file mode 100644
index 0000000..d110063
--- /dev/null
+++ b/src/main/java/com/aolda/itda/exception/CustomException.java
@@ -0,0 +1,19 @@
+package com.aolda.itda.exception;
+
+import lombok.Getter;
+
+@Getter
+public class CustomException extends RuntimeException{
+    private ErrorCode errorCode;
+
+    private String info;
+
+    public CustomException(ErrorCode errorCode) {
+        this.errorCode = errorCode;
+    }
+
+    public CustomException(ErrorCode errorCode, String info){
+        this.errorCode = errorCode;
+        this.info = info;
+    }
+}
diff --git a/src/main/java/com/aolda/itda/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java
new file mode 100644
index 0000000..6bcb643
--- /dev/null
+++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java
@@ -0,0 +1,16 @@
+package com.aolda.itda.exception;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@RequiredArgsConstructor
+public enum ErrorCode {
+
+    // User
+    INVALID_USER_INFO(HttpStatus.BAD_REQUEST, "잘못된 회원 정보입니다");
+
+    private final HttpStatus status;
+    private final String message;
+}
diff --git a/src/main/java/com/aolda/itda/exception/ErrorResponse.java b/src/main/java/com/aolda/itda/exception/ErrorResponse.java
new file mode 100644
index 0000000..1bec060
--- /dev/null
+++ b/src/main/java/com/aolda/itda/exception/ErrorResponse.java
@@ -0,0 +1,29 @@
+package com.aolda.itda.exception;
+
+import lombok.Builder;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+@Getter
+@Builder
+public class ErrorResponse {
+
+    private final HttpStatus status;  // HTTP 상태 코드
+    private final String code;        // 에러 코드
+    private final String message;     // 에러 메시지
+
+    public static ResponseEntity<ErrorResponse> fromException(CustomException e) {
+        String message = e.getErrorCode().getMessage();
+        if (e.getInfo() != null) {
+            message += " " + e.getInfo(); // 추가 정보가 있는 경우 결합
+        }
+        return ResponseEntity
+                .status(e.getErrorCode().getStatus())
+                .body(ErrorResponse.builder()
+                        .status(e.getErrorCode().getStatus())
+                        .code(e.getErrorCode().name())
+                        .message(message)
+                        .build());
+    }
+}
diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java
new file mode 100644
index 0000000..5f071ee
--- /dev/null
+++ b/src/main/java/com/aolda/itda/service/AuthService.java
@@ -0,0 +1,159 @@
+package com.aolda.itda.service;
+
+import com.aolda.itda.dto.auth.LoginRequestDTO;
+import com.aolda.itda.dto.auth.LoginResponseDTO;
+import com.aolda.itda.dto.auth.ProjectIdAndNameDTO;
+import com.aolda.itda.dto.auth.ProjectRoleDTO;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.*;
+
+@Service
+@RequiredArgsConstructor
+public class AuthService {
+
+    @Value("${spring.server.keystone}")
+    private String keystone;
+    @Value("${spring.server.admin-id}")
+    private String adminId;
+    @Value("${spring.server.admin-password}")
+    private String adminPassword;
+    private final RestTemplate restTemplate = new RestTemplate();
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    // 사용자 로그인 후 토큰 발행 및 Role 반환
+    public LoginResponseDTO userLogin(HttpServletResponse response, LoginRequestDTO loginRequestDTO) throws JsonProcessingException {
+        Map<String, String> user = getToken(loginRequestDTO.getId(), loginRequestDTO.getPassword());
+
+        String userId = user.get("id");
+        String token = user.get("token");
+
+        if (userId == null || token == null) {
+            throw new CustomException(ErrorCode.INVALID_USER_INFO);
+        }
+
+        response.addHeader("X-Subject-Token", token);
+        return LoginResponseDTO.builder()
+                .isAdmin(false)
+                .lists(getProjectsWithUser(user))
+                .build();
+    }
+
+    // 특정 사용자의 토큰 발행
+    private Map<String, String> getToken(String id, String password) {
+
+        String url = keystone + "/auth/tokens";
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+
+        String requestBody = "{\n" +
+                "    \"auth\": {\n" +
+                "        \"identity\": {\n" +
+                "            \"methods\": [\n" +
+                "                \"password\"\n" +
+                "            ],\n" +
+                "            \"password\": {\n" +
+                "                \"user\": {\n" +
+                "                    \"name\": \""+ id + "\",\n" +
+                "                    \"domain\": {\n" +
+                "                        \"name\": \"Default\"\n" +
+                "                    },\n" +
+                "                    \"password\": \"" + password + "\"\n" +
+                "                }\n" +
+                "            }\n" +
+                "        }\n" +
+                "    }\n" +
+                "}";
+
+        HttpEntity<String> requestEntity = new HttpEntity<>(requestBody, headers);
+        ResponseEntity<Map> res = restTemplate.postForEntity(url, requestEntity, Map.class);
+
+        Map<String, Object> resToken = (Map<String, Object>) res.getBody().get("token");
+        Map<String, Object> resUser = (Map<String, Object>) resToken.get("user");
+        String userId = (String) resUser.get("id");
+        String token = res.getHeaders().getFirst("X-Subject-Token");
+
+        return Map.of("id", userId,
+                    "token", token);
+    }
+
+    // 특정 사용자의 프로젝트별 Role 반환
+    private List<ProjectRoleDTO> getRolesWithProjects(Map<String, String> user) throws JsonProcessingException {
+        String userId = user.get("id");
+        String token = user.get("token");
+
+        if (userId == null || token == null) {
+            throw new CustomException(ErrorCode.INVALID_USER_INFO);
+        }
+
+        String url = keystone + "/role_assignments?user.id=" + userId + "&effective&include_names=true";
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("X-Auth-Token", getAdminToken());
+
+        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
+        ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
+
+        JsonNode node = objectMapper.readTree(res.getBody());
+        ArrayNode arrayNode = (ArrayNode) node.get("role_assignments");
+
+        List<ProjectRoleDTO> lists = new ArrayList<>();
+
+        for (JsonNode assignment : arrayNode) {
+
+            String projectName = assignment.path("scope").path("project").path("name").asText();
+            String roleName = assignment.path("role").path("name").asText();
+
+            ProjectRoleDTO projectRoleDTO = new ProjectRoleDTO(projectName, roleName);
+            lists.add(projectRoleDTO);
+
+        }
+
+        return lists;
+    }
+
+    // 관리자용 토큰 발행
+    public String getAdminToken() {
+        Map<String, String> user = getToken(adminId, adminPassword);
+        return user.get("token");
+    }
+
+    private List<ProjectIdAndNameDTO> getProjectsWithUser(Map<String, String> user) throws JsonProcessingException {
+        String userId = user.get("id");
+        String token = user.get("token");
+        if (userId == null || token == null) {
+            throw new CustomException(ErrorCode.INVALID_USER_INFO);
+        }
+
+        String url = keystone + "/users/" + userId + "/projects";
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("X-Auth-Token", getAdminToken());
+
+        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
+        ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
+
+        JsonNode node = objectMapper.readTree(res.getBody());
+        ArrayNode arrayNode = (ArrayNode) node.get("projects");
+
+        List<ProjectIdAndNameDTO> lists = new ArrayList<>();
+
+        for (JsonNode assignment : arrayNode) {
+            String projectId = assignment.path("id").asText();
+            String projectName = assignment.path("name").asText();
+            lists.add(new ProjectIdAndNameDTO(projectId, projectName));
+        }
+        return lists;
+    }
+}
-- 
GitLab


From 0f0003d97c25c5725945ac08e460e2fdb289f78b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Fri, 28 Feb 2025 14:47:36 +0900
Subject: [PATCH 02/41] =?UTF-8?q?feat:=20=EC=98=88=EC=99=B8=20=EC=B2=98?=
 =?UTF-8?q?=EB=A6=AC=EC=9A=A9=20=ED=95=B8=EB=93=A4=EB=9F=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 build.gradle | 1 -
 1 file changed, 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 5cef9f6..8b2abc8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -29,7 +29,6 @@ dependencies {
     compileOnly 'org.projectlombok:lombok'
     runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
     runtimeOnly 'com.mysql:mysql-connector-j'
-    implementation 'org.pacesys:openstack4j:3.1.0'
     annotationProcessor 'org.projectlombok:lombok'
     testImplementation 'org.springframework.boot:spring-boot-starter-test'
     testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
-- 
GitLab


From e76498c5df30f39d4383b6361958ffaa770c9563 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Fri, 28 Feb 2025 22:27:45 +0900
Subject: [PATCH 03/41] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EB=B0=9C?=
 =?UTF-8?q?=ED=96=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 build.gradle                                          | 1 +
 src/main/java/com/aolda/itda/service/AuthService.java | 2 --
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/build.gradle b/build.gradle
index 8b2abc8..5cef9f6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -29,6 +29,7 @@ dependencies {
     compileOnly 'org.projectlombok:lombok'
     runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
     runtimeOnly 'com.mysql:mysql-connector-j'
+    implementation 'org.pacesys:openstack4j:3.1.0'
     annotationProcessor 'org.projectlombok:lombok'
     testImplementation 'org.springframework.boot:spring-boot-starter-test'
     testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java
index 5f071ee..e64baf7 100644
--- a/src/main/java/com/aolda/itda/service/AuthService.java
+++ b/src/main/java/com/aolda/itda/service/AuthService.java
@@ -65,11 +65,9 @@ public class AuthService {
                 "            ],\n" +
                 "            \"password\": {\n" +
                 "                \"user\": {\n" +
-                "                    \"name\": \""+ id + "\",\n" +
                 "                    \"domain\": {\n" +
                 "                        \"name\": \"Default\"\n" +
                 "                    },\n" +
-                "                    \"password\": \"" + password + "\"\n" +
                 "                }\n" +
                 "            }\n" +
                 "        }\n" +
-- 
GitLab


From b1ea5b7cb79a4b7aa5aee99328c94e347cb2eb06 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Sun, 2 Mar 2025 23:35:40 +0900
Subject: [PATCH 04/41] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EB=B0=9C?=
 =?UTF-8?q?=ED=96=89=EC=8B=9C=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8?=
 =?UTF-8?q?=EB=B3=84=20Role=20=EB=B0=98=ED=99=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/aolda/itda/service/AuthService.java   | 34 ++-----------------
 1 file changed, 3 insertions(+), 31 deletions(-)

diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java
index e64baf7..cbd20a1 100644
--- a/src/main/java/com/aolda/itda/service/AuthService.java
+++ b/src/main/java/com/aolda/itda/service/AuthService.java
@@ -2,7 +2,6 @@ package com.aolda.itda.service;
 
 import com.aolda.itda.dto.auth.LoginRequestDTO;
 import com.aolda.itda.dto.auth.LoginResponseDTO;
-import com.aolda.itda.dto.auth.ProjectIdAndNameDTO;
 import com.aolda.itda.dto.auth.ProjectRoleDTO;
 import com.aolda.itda.exception.CustomException;
 import com.aolda.itda.exception.ErrorCode;
@@ -45,8 +44,7 @@ public class AuthService {
 
         response.addHeader("X-Subject-Token", token);
         return LoginResponseDTO.builder()
-                .isAdmin(false)
-                .lists(getProjectsWithUser(user))
+                .lists(getRolesWithProjects(user))
                 .build();
     }
 
@@ -65,9 +63,11 @@ public class AuthService {
                 "            ],\n" +
                 "            \"password\": {\n" +
                 "                \"user\": {\n" +
+                "                    \"name\": \""+ id + "\",\n" +
                 "                    \"domain\": {\n" +
                 "                        \"name\": \"Default\"\n" +
                 "                    },\n" +
+                "                    \"password\": \"" + password + "\"\n" +
                 "                }\n" +
                 "            }\n" +
                 "        }\n" +
@@ -126,32 +126,4 @@ public class AuthService {
         Map<String, String> user = getToken(adminId, adminPassword);
         return user.get("token");
     }
-
-    private List<ProjectIdAndNameDTO> getProjectsWithUser(Map<String, String> user) throws JsonProcessingException {
-        String userId = user.get("id");
-        String token = user.get("token");
-        if (userId == null || token == null) {
-            throw new CustomException(ErrorCode.INVALID_USER_INFO);
-        }
-
-        String url = keystone + "/users/" + userId + "/projects";
-
-        HttpHeaders headers = new HttpHeaders();
-        headers.set("X-Auth-Token", getAdminToken());
-
-        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
-        ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
-
-        JsonNode node = objectMapper.readTree(res.getBody());
-        ArrayNode arrayNode = (ArrayNode) node.get("projects");
-
-        List<ProjectIdAndNameDTO> lists = new ArrayList<>();
-
-        for (JsonNode assignment : arrayNode) {
-            String projectId = assignment.path("id").asText();
-            String projectName = assignment.path("name").asText();
-            lists.add(new ProjectIdAndNameDTO(projectId, projectName));
-        }
-        return lists;
-    }
 }
-- 
GitLab


From 74f69b1c388c06a0ae7555717b9070fd61675cbc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Mon, 3 Mar 2025 21:02:38 +0900
Subject: [PATCH 05/41] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EB=B0=9C?=
 =?UTF-8?q?=ED=96=89=EC=8B=9C=20=EC=B0=B8=EC=97=AC=20=ED=94=84=EB=A1=9C?=
 =?UTF-8?q?=EC=A0=9D=ED=8A=B8=EB=A7=8C=20=EB=B0=98=ED=99=98=ED=95=98?=
 =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/aolda/itda/service/AuthService.java   | 32 ++++++++++++++++++-
 1 file changed, 31 insertions(+), 1 deletion(-)

diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java
index cbd20a1..5f071ee 100644
--- a/src/main/java/com/aolda/itda/service/AuthService.java
+++ b/src/main/java/com/aolda/itda/service/AuthService.java
@@ -2,6 +2,7 @@ package com.aolda.itda.service;
 
 import com.aolda.itda.dto.auth.LoginRequestDTO;
 import com.aolda.itda.dto.auth.LoginResponseDTO;
+import com.aolda.itda.dto.auth.ProjectIdAndNameDTO;
 import com.aolda.itda.dto.auth.ProjectRoleDTO;
 import com.aolda.itda.exception.CustomException;
 import com.aolda.itda.exception.ErrorCode;
@@ -44,7 +45,8 @@ public class AuthService {
 
         response.addHeader("X-Subject-Token", token);
         return LoginResponseDTO.builder()
-                .lists(getRolesWithProjects(user))
+                .isAdmin(false)
+                .lists(getProjectsWithUser(user))
                 .build();
     }
 
@@ -126,4 +128,32 @@ public class AuthService {
         Map<String, String> user = getToken(adminId, adminPassword);
         return user.get("token");
     }
+
+    private List<ProjectIdAndNameDTO> getProjectsWithUser(Map<String, String> user) throws JsonProcessingException {
+        String userId = user.get("id");
+        String token = user.get("token");
+        if (userId == null || token == null) {
+            throw new CustomException(ErrorCode.INVALID_USER_INFO);
+        }
+
+        String url = keystone + "/users/" + userId + "/projects";
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("X-Auth-Token", getAdminToken());
+
+        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
+        ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
+
+        JsonNode node = objectMapper.readTree(res.getBody());
+        ArrayNode arrayNode = (ArrayNode) node.get("projects");
+
+        List<ProjectIdAndNameDTO> lists = new ArrayList<>();
+
+        for (JsonNode assignment : arrayNode) {
+            String projectId = assignment.path("id").asText();
+            String projectName = assignment.path("name").asText();
+            lists.add(new ProjectIdAndNameDTO(projectId, projectName));
+        }
+        return lists;
+    }
 }
-- 
GitLab


From e5489649279e1bf7751e426784d1d368280d24af Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Mon, 3 Mar 2025 22:56:29 +0900
Subject: [PATCH 06/41] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90?=
 =?UTF-8?q?=EC=9D=98=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20Role=20api?=
 =?UTF-8?q?=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../aolda/itda/controller/AuthController.java | 12 +++--
 .../com/aolda/itda/exception/ErrorCode.java   |  5 +-
 .../com/aolda/itda/service/AuthService.java   | 47 +++++++++++++++----
 3 files changed, 51 insertions(+), 13 deletions(-)

diff --git a/src/main/java/com/aolda/itda/controller/AuthController.java b/src/main/java/com/aolda/itda/controller/AuthController.java
index 274ad8f..79046d2 100644
--- a/src/main/java/com/aolda/itda/controller/AuthController.java
+++ b/src/main/java/com/aolda/itda/controller/AuthController.java
@@ -6,10 +6,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.RequiredArgsConstructor;
 import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 @RestController
 @RequestMapping("/api/auth")
@@ -24,4 +21,11 @@ public class AuthController {
 
         return ResponseEntity.ok(authService.userLogin(response, loginRequestDTO));
     }
+
+    @GetMapping("/role")
+    public ResponseEntity<Object> roleWithinProject(@RequestHeader("X-Subject-Token") String token,
+                                                    @RequestParam String projectId) throws JsonProcessingException {
+
+        return ResponseEntity.ok(authService.getBestRoleWithinProject(token, projectId));
+    }
 }
diff --git a/src/main/java/com/aolda/itda/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java
index 6bcb643..0916e71 100644
--- a/src/main/java/com/aolda/itda/exception/ErrorCode.java
+++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java
@@ -9,7 +9,10 @@ import org.springframework.http.HttpStatus;
 public enum ErrorCode {
 
     // User
-    INVALID_USER_INFO(HttpStatus.BAD_REQUEST, "잘못된 회원 정보입니다");
+    INVALID_USER_INFO(HttpStatus.BAD_REQUEST, "잘못된 회원 정보입니다"),
+
+    // Token
+    INVALID_TOKEN(HttpStatus.BAD_REQUEST, "잘못된 토큰입니다");
 
     private final HttpStatus status;
     private final String message;
diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java
index 5f071ee..6fea01e 100644
--- a/src/main/java/com/aolda/itda/service/AuthService.java
+++ b/src/main/java/com/aolda/itda/service/AuthService.java
@@ -15,6 +15,9 @@ import lombok.RequiredArgsConstructor;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.*;
 import org.springframework.stereotype.Service;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.HttpStatusCodeException;
+import org.springframework.web.client.RestClientException;
 import org.springframework.web.client.RestTemplate;
 
 import java.util.*;
@@ -88,8 +91,15 @@ public class AuthService {
                     "token", token);
     }
 
-    // 특정 사용자의 프로젝트별 Role 반환
-    private List<ProjectRoleDTO> getRolesWithProjects(Map<String, String> user) throws JsonProcessingException {
+    // 특정 사용자의 특정 프로젝트 내 최고 권한 반환
+    public Map<String, String> getBestRoleWithinProject(String token, String projectId) throws JsonProcessingException {
+        return getBestRoleWithinProject(Map.of(
+                "id", validateTokenAndGetUserId(token),
+                "token", token),
+                projectId);
+    }
+
+    private Map<String, String> getBestRoleWithinProject(Map<String, String> user, String projectId) throws JsonProcessingException {
         String userId = user.get("id");
         String token = user.get("token");
 
@@ -97,7 +107,7 @@ public class AuthService {
             throw new CustomException(ErrorCode.INVALID_USER_INFO);
         }
 
-        String url = keystone + "/role_assignments?user.id=" + userId + "&effective&include_names=true";
+        String url = keystone + "/role_assignments?user.id=" + userId + "&effective&include_names=true&scope.project.id=" + projectId;
 
         HttpHeaders headers = new HttpHeaders();
         headers.set("X-Auth-Token", getAdminToken());
@@ -108,19 +118,23 @@ public class AuthService {
         JsonNode node = objectMapper.readTree(res.getBody());
         ArrayNode arrayNode = (ArrayNode) node.get("role_assignments");
 
-        List<ProjectRoleDTO> lists = new ArrayList<>();
+        String bestRole = "reader";
 
         for (JsonNode assignment : arrayNode) {
 
-            String projectName = assignment.path("scope").path("project").path("name").asText();
             String roleName = assignment.path("role").path("name").asText();
 
-            ProjectRoleDTO projectRoleDTO = new ProjectRoleDTO(projectName, roleName);
-            lists.add(projectRoleDTO);
+            if (roleName.equals("admin")) { // admin인 경우
+                bestRole = roleName;
+            } else if (roleName.equals("manager") && !bestRole.equals("admin")) { // 최고 권한이 admin이 아닌 경우
+                bestRole = roleName;
+            } else if (roleName.equals("member") && bestRole.equals("reader")) { // 최고 권한이 reader인 경우
+                bestRole = roleName;
+            }
 
         }
 
-        return lists;
+        return Map.of("role", bestRole);
     }
 
     // 관리자용 토큰 발행
@@ -129,6 +143,7 @@ public class AuthService {
         return user.get("token");
     }
 
+    // 특정 사용자의 참여 프로젝트 반환
     private List<ProjectIdAndNameDTO> getProjectsWithUser(Map<String, String> user) throws JsonProcessingException {
         String userId = user.get("id");
         String token = user.get("token");
@@ -156,4 +171,20 @@ public class AuthService {
         }
         return lists;
     }
+
+    private String validateTokenAndGetUserId(String token) throws JsonProcessingException {
+        String url = keystone + "/auth/tokens";
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("X-Auth-Token", getAdminToken());
+        headers.set("X-Subject-Token", token);
+        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
+        ResponseEntity<String> res;
+        try {
+            res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
+        } catch (HttpClientErrorException.NotFound e) {
+            throw new CustomException(ErrorCode.INVALID_TOKEN);
+        }
+        return objectMapper.readTree(res.getBody()).path("token").path("user").path("id").asText();
+
+    }
 }
-- 
GitLab


From cf23a24328437198177e86a2ad91b737db238120 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Mon, 3 Mar 2025 23:11:53 +0900
Subject: [PATCH 07/41] =?UTF-8?q?chore:=20=ED=95=84=EC=9A=94=EC=97=86?=
 =?UTF-8?q?=EB=8A=94=20dto=20=EB=B0=8F=20import=20=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/aolda/itda/dto/auth/ProjectRoleDTO.java | 17 -----------------
 .../com/aolda/itda/service/AuthService.java     |  3 ---
 2 files changed, 20 deletions(-)
 delete mode 100644 src/main/java/com/aolda/itda/dto/auth/ProjectRoleDTO.java

diff --git a/src/main/java/com/aolda/itda/dto/auth/ProjectRoleDTO.java b/src/main/java/com/aolda/itda/dto/auth/ProjectRoleDTO.java
deleted file mode 100644
index d3b9b8e..0000000
--- a/src/main/java/com/aolda/itda/dto/auth/ProjectRoleDTO.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.aolda.itda.dto.auth;
-
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-import java.util.List;
-
-@Getter
-@NoArgsConstructor
-@AllArgsConstructor
-@Builder
-public class ProjectRoleDTO {
-    private String projectName;
-    private String roleName;
-}
diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java
index 6fea01e..88062f7 100644
--- a/src/main/java/com/aolda/itda/service/AuthService.java
+++ b/src/main/java/com/aolda/itda/service/AuthService.java
@@ -3,7 +3,6 @@ package com.aolda.itda.service;
 import com.aolda.itda.dto.auth.LoginRequestDTO;
 import com.aolda.itda.dto.auth.LoginResponseDTO;
 import com.aolda.itda.dto.auth.ProjectIdAndNameDTO;
-import com.aolda.itda.dto.auth.ProjectRoleDTO;
 import com.aolda.itda.exception.CustomException;
 import com.aolda.itda.exception.ErrorCode;
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -16,8 +15,6 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.*;
 import org.springframework.stereotype.Service;
 import org.springframework.web.client.HttpClientErrorException;
-import org.springframework.web.client.HttpStatusCodeException;
-import org.springframework.web.client.RestClientException;
 import org.springframework.web.client.RestTemplate;
 
 import java.util.*;
-- 
GitLab


From caac306fd9c3c19012d948b3c7f6be6256a3aae0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Mon, 10 Mar 2025 16:29:35 +0900
Subject: [PATCH 08/41] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?=
 =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20?=
 =?UTF-8?q?=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../aolda/itda/dto/auth/LoginResponseDTO.java |  2 +-
 .../com/aolda/itda/service/AuthService.java   | 83 +++++++++++++++++--
 2 files changed, 78 insertions(+), 7 deletions(-)

diff --git a/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java b/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java
index a5aa14a..33ba32f 100644
--- a/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java
+++ b/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java
@@ -13,5 +13,5 @@ import java.util.List;
 @Builder
 public class LoginResponseDTO {
     private Boolean isAdmin;
-    private List<ProjectIdAndNameDTO> lists;
+    private List<ProjectIdAndNameDTO> projects;
 }
diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java
index 88062f7..80f7196 100644
--- a/src/main/java/com/aolda/itda/service/AuthService.java
+++ b/src/main/java/com/aolda/itda/service/AuthService.java
@@ -38,15 +38,16 @@ public class AuthService {
 
         String userId = user.get("id");
         String token = user.get("token");
+        String systemToken = getSystemToken(userId, loginRequestDTO.getPassword());
 
         if (userId == null || token == null) {
             throw new CustomException(ErrorCode.INVALID_USER_INFO);
         }
 
-        response.addHeader("X-Subject-Token", token);
+        response.addHeader("X-Subject-Token", systemToken != null ? systemToken : token);
         return LoginResponseDTO.builder()
-                .isAdmin(false)
-                .lists(getProjectsWithUser(user))
+                .isAdmin(systemToken != null)
+                .projects(getProjectsWithUser(user))
                 .build();
     }
 
@@ -88,6 +89,50 @@ public class AuthService {
                     "token", token);
     }
 
+    private String getSystemToken(String id, String password) {
+
+        String url = keystone + "/auth/tokens";
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+
+        String requestBody = "{\n" +
+                "    \"auth\": {\n" +
+                "        \"identity\": {\n" +
+                "            \"methods\": [\n" +
+                "                \"password\"\n" +
+                "            ],\n" +
+                "            \"password\": {\n" +
+                "                \"user\": {\n" +
+                "                    \"id\": \"" + id + "\",\n" +
+                "                    \"password\": \"" + password + "\"\n" +
+                "                }\n" +
+                "            }\n" +
+                "        },\n" +
+                "        \"scope\": {\n" +
+                "            \"system\": {\n" +
+                "                \"all\": true\n" +
+                "            }\n" +
+                "        }\n" +
+                "    }\n" +
+                "}";
+
+        HttpEntity<String> requestEntity;
+        ResponseEntity<Map> res;
+        try {
+            requestEntity = new HttpEntity<>(requestBody, headers);
+            res = restTemplate.postForEntity(url, requestEntity, Map.class);
+        } catch (RuntimeException e) {
+            return null;
+        }
+
+        Map<String, Object> resToken = (Map<String, Object>) res.getBody().get("token");
+        Map<String, Object> resUser = (Map<String, Object>) resToken.get("user");
+        String userId = (String) resUser.get("id");
+        String token = res.getHeaders().getFirst("X-Subject-Token");
+
+        return token;
+    }
+
     // 특정 사용자의 특정 프로젝트 내 최고 권한 반환
     public Map<String, String> getBestRoleWithinProject(String token, String projectId) throws JsonProcessingException {
         return getBestRoleWithinProject(Map.of(
@@ -107,7 +152,7 @@ public class AuthService {
         String url = keystone + "/role_assignments?user.id=" + userId + "&effective&include_names=true&scope.project.id=" + projectId;
 
         HttpHeaders headers = new HttpHeaders();
-        headers.set("X-Auth-Token", getAdminToken());
+        headers.set("X-Auth-Token", token);
 
         HttpEntity<String> requestEntity = new HttpEntity<>(headers);
         ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
@@ -151,7 +196,7 @@ public class AuthService {
         String url = keystone + "/users/" + userId + "/projects";
 
         HttpHeaders headers = new HttpHeaders();
-        headers.set("X-Auth-Token", getAdminToken());
+        headers.set("X-Auth-Token", token);
 
         HttpEntity<String> requestEntity = new HttpEntity<>(headers);
         ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
@@ -172,7 +217,7 @@ public class AuthService {
     private String validateTokenAndGetUserId(String token) throws JsonProcessingException {
         String url = keystone + "/auth/tokens";
         HttpHeaders headers = new HttpHeaders();
-        headers.set("X-Auth-Token", getAdminToken());
+        headers.set("X-Auth-Token", token);
         headers.set("X-Subject-Token", token);
         HttpEntity<String> requestEntity = new HttpEntity<>(headers);
         ResponseEntity<String> res;
@@ -184,4 +229,30 @@ public class AuthService {
         return objectMapper.readTree(res.getBody()).path("token").path("user").path("id").asText();
 
     }
+
+    private Boolean isAdmin(Map<String, String> user) throws JsonProcessingException {
+        String url = keystone + "/role_assignments?user.id=" + user.get("id") + "&scope.system&include_names";
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("X-Auth-Token", user.get("token"));
+        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
+        ResponseEntity<String> res;
+        try {
+            res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
+        } catch (RuntimeException e) {
+            e.printStackTrace();
+            System.out.println("runtime");
+            return false;
+        }
+        JsonNode node = objectMapper.readTree(res.getBody()).path("role_assignments");
+        String system_all = node.path("scope").path("system").path("all").asText();
+        String role = node.path("role").path("name").asText();
+        System.out.println("role: " + role);
+        if (system_all.equals("true") && role.equals("admin")) {
+            System.out.println(system_all);
+            return true;
+        }
+        System.out.println("hi");
+        return false;
+
+    }
 }
-- 
GitLab


From 7320a6e16e89547ad69925d35196b7d0d57a43b3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Mon, 10 Mar 2025 17:31:35 +0900
Subject: [PATCH 09/41] =?UTF-8?q?fix:=20project=20token=EC=9C=BC=EB=A1=9C?=
 =?UTF-8?q?=20project=20role=20=ED=99=95=EC=9D=B8=ED=95=98=EB=8F=84?=
 =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/aolda/itda/service/AuthService.java   | 52 ++++++++++++++++++-
 1 file changed, 51 insertions(+), 1 deletion(-)

diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java
index 80f7196..1dea9a2 100644
--- a/src/main/java/com/aolda/itda/service/AuthService.java
+++ b/src/main/java/com/aolda/itda/service/AuthService.java
@@ -15,6 +15,7 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.*;
 import org.springframework.stereotype.Service;
 import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestClientException;
 import org.springframework.web.client.RestTemplate;
 
 import java.util.*;
@@ -133,11 +134,59 @@ public class AuthService {
         return token;
     }
 
+    private String getProjectToken(String unscopedToken, String projectId) {
+
+        String url = keystone + "/auth/tokens";
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+
+
+        String requestBody = "{\n" +
+                "    \"auth\": {\n" +
+                "        \"identity\": {\n" +
+                "            \"methods\": [\n" +
+                "                \"token\"\n" +
+                "            ],\n" +
+                "            \"token\": {\n" +
+                "                \"id\": \"" + unscopedToken +"\"\n" +
+                "            }\n" +
+                "        },\n" +
+                "        \"scope\": {\n" +
+                "            \"project\": {\n" +
+                "                \"id\": \""+ projectId +"\"\n" +
+                "            }\n" +
+                "        }\n" +
+                "    }\n" +
+                "}";
+
+        HttpEntity<String> requestEntity;
+        ResponseEntity<Map> res;
+        try {
+            requestEntity = new HttpEntity<>(requestBody, headers);
+            res = restTemplate.postForEntity(url, requestEntity, Map.class);
+        } catch (HttpClientErrorException.Forbidden e) {
+            return unscopedToken;
+        }
+        catch (RuntimeException e) {
+            e.printStackTrace();
+            throw new CustomException(ErrorCode.INVALID_TOKEN);
+        }
+
+        Map<String, Object> resToken = (Map<String, Object>) res.getBody().get("token");
+        Map<String, Object> resUser = (Map<String, Object>) resToken.get("user");
+        String userId = (String) resUser.get("id");
+        String token = res.getHeaders().getFirst("X-Subject-Token");
+
+        return token;
+    }
+
+
     // 특정 사용자의 특정 프로젝트 내 최고 권한 반환
     public Map<String, String> getBestRoleWithinProject(String token, String projectId) throws JsonProcessingException {
+
         return getBestRoleWithinProject(Map.of(
                 "id", validateTokenAndGetUserId(token),
-                "token", token),
+                "token", getProjectToken(token, projectId)),
                 projectId);
     }
 
@@ -224,6 +273,7 @@ public class AuthService {
         try {
             res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
         } catch (HttpClientErrorException.NotFound e) {
+            System.out.println("validate");
             throw new CustomException(ErrorCode.INVALID_TOKEN);
         }
         return objectMapper.readTree(res.getBody()).path("token").path("user").path("id").asText();
-- 
GitLab


From f06d3bce2771390e445c47b0d77f03baaa163653 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Sun, 9 Mar 2025 19:20:50 +0900
Subject: [PATCH 10/41] =?UTF-8?q?feat:=20=EB=9D=BC=EC=9A=B0=ED=8C=85,=20?=
 =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EB=93=B1=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?=
 =?UTF-8?q?=EC=83=9D=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/aolda/itda/entity/BaseTimeEntity.java | 26 +++++++++++
 .../itda/entity/certificate/Certificate.java  | 46 +++++++++++++++++++
 .../itda/entity/certificate/Challenge.java    |  5 ++
 .../itda/entity/forwarding/Forwarding.java    | 39 ++++++++++++++++
 .../com/aolda/itda/entity/log/Action.java     |  5 ++
 .../java/com/aolda/itda/entity/log/Log.java   | 40 ++++++++++++++++
 .../com/aolda/itda/entity/log/ObjectType.java |  5 ++
 .../aolda/itda/entity/routing/Routing.java    | 42 +++++++++++++++++
 .../java/com/aolda/itda/entity/user/User.java | 24 ++++++++++
 9 files changed, 232 insertions(+)
 create mode 100644 src/main/java/com/aolda/itda/entity/BaseTimeEntity.java
 create mode 100644 src/main/java/com/aolda/itda/entity/certificate/Certificate.java
 create mode 100644 src/main/java/com/aolda/itda/entity/certificate/Challenge.java
 create mode 100644 src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java
 create mode 100644 src/main/java/com/aolda/itda/entity/log/Action.java
 create mode 100644 src/main/java/com/aolda/itda/entity/log/Log.java
 create mode 100644 src/main/java/com/aolda/itda/entity/log/ObjectType.java
 create mode 100644 src/main/java/com/aolda/itda/entity/routing/Routing.java
 create mode 100644 src/main/java/com/aolda/itda/entity/user/User.java

diff --git a/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java b/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java
new file mode 100644
index 0000000..4460eb7
--- /dev/null
+++ b/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java
@@ -0,0 +1,26 @@
+package com.aolda.itda.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.EntityListeners;
+import jakarta.persistence.MappedSuperclass;
+import lombok.Getter;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+
+@Getter
+@MappedSuperclass
+@EntityListeners(AuditingEntityListener.class)
+public abstract class BaseTimeEntity {
+
+    @CreatedDate
+    @Column(updatable = false)
+    private LocalDateTime createdAt;
+
+    @LastModifiedDate
+    @Column(name = "updated_at")
+    private LocalDateTime updatedAt;
+
+}
diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
new file mode 100644
index 0000000..76253a1
--- /dev/null
+++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
@@ -0,0 +1,46 @@
+package com.aolda.itda.entity.certificate;
+
+import com.aolda.itda.entity.BaseTimeEntity;
+import com.aolda.itda.entity.user.User;
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "routing")
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Certificate extends BaseTimeEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(nullable = false)
+    private Long certificateId;
+
+    @OneToOne
+    @JoinColumn(nullable = false, name = "user_id")
+    private User user;
+
+    private String projectId;
+
+    private String domain;
+
+    private String email;
+
+    private LocalDateTime expiredAt;
+
+    @Enumerated(EnumType.STRING)
+    private Challenge challenge;
+
+    private Boolean isDeleted;
+
+    private String metadata;
+
+
+}
diff --git a/src/main/java/com/aolda/itda/entity/certificate/Challenge.java b/src/main/java/com/aolda/itda/entity/certificate/Challenge.java
new file mode 100644
index 0000000..14713bf
--- /dev/null
+++ b/src/main/java/com/aolda/itda/entity/certificate/Challenge.java
@@ -0,0 +1,5 @@
+package com.aolda.itda.entity.certificate;
+
+public enum Challenge {
+    HTTP, DNS_CLOUDFLARE
+}
diff --git a/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java
new file mode 100644
index 0000000..0288af8
--- /dev/null
+++ b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java
@@ -0,0 +1,39 @@
+package com.aolda.itda.entity.forwarding;
+
+import com.aolda.itda.entity.BaseTimeEntity;
+import com.aolda.itda.entity.user.User;
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "forwarding")
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Forwarding extends BaseTimeEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(nullable = false)
+    private Long forwardingId;
+
+    @OneToOne
+    @JoinColumn(name = "user_id")
+    private User user;
+
+    private String projectId;
+
+    private String serverIp;
+
+    private String serverPort;
+
+    private String instanceIp;
+
+    private String instancePort;
+
+    private Boolean isDeleted;
+}
diff --git a/src/main/java/com/aolda/itda/entity/log/Action.java b/src/main/java/com/aolda/itda/entity/log/Action.java
new file mode 100644
index 0000000..a620f87
--- /dev/null
+++ b/src/main/java/com/aolda/itda/entity/log/Action.java
@@ -0,0 +1,5 @@
+package com.aolda.itda.entity.log;
+
+public enum Action {
+    CREATE, UPDATE, DELETE
+}
diff --git a/src/main/java/com/aolda/itda/entity/log/Log.java b/src/main/java/com/aolda/itda/entity/log/Log.java
new file mode 100644
index 0000000..fd8f460
--- /dev/null
+++ b/src/main/java/com/aolda/itda/entity/log/Log.java
@@ -0,0 +1,40 @@
+package com.aolda.itda.entity.log;
+
+import com.aolda.itda.entity.BaseTimeEntity;
+import com.aolda.itda.entity.user.User;
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "log")
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Log extends BaseTimeEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(nullable = false)
+    private Long logId;
+
+    @OneToOne
+    @JoinColumn(name = "user_id", nullable = false)
+    private User user;
+
+    private String projectId;
+
+    @Enumerated(EnumType.STRING)
+    private ObjectType objectType;
+
+    private Long objectId;
+
+    @Enumerated(EnumType.STRING)
+    private Action action;
+
+    private String metadata;
+
+}
diff --git a/src/main/java/com/aolda/itda/entity/log/ObjectType.java b/src/main/java/com/aolda/itda/entity/log/ObjectType.java
new file mode 100644
index 0000000..5310315
--- /dev/null
+++ b/src/main/java/com/aolda/itda/entity/log/ObjectType.java
@@ -0,0 +1,5 @@
+package com.aolda.itda.entity.log;
+
+public enum ObjectType {
+    ROUTING, CERTIFICATE, FORWARDING
+}
diff --git a/src/main/java/com/aolda/itda/entity/routing/Routing.java b/src/main/java/com/aolda/itda/entity/routing/Routing.java
new file mode 100644
index 0000000..2eb9bd1
--- /dev/null
+++ b/src/main/java/com/aolda/itda/entity/routing/Routing.java
@@ -0,0 +1,42 @@
+package com.aolda.itda.entity.routing;
+
+import com.aolda.itda.entity.BaseTimeEntity;
+import com.aolda.itda.entity.certificate.Certificate;
+import com.aolda.itda.entity.user.User;
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "routing")
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Routing extends BaseTimeEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(nullable = false)
+    private Long routingId;
+
+    @OneToOne
+    @JoinColumn(name = "user_id", nullable = false)
+    private User user;
+
+    @OneToOne
+    @JoinColumn(name = "certificate_id")
+    private Certificate certificate;
+
+    private String projectId;
+
+    private String domain;
+
+    private String instanceIp;
+
+    private Boolean isDeleted;
+
+
+}
diff --git a/src/main/java/com/aolda/itda/entity/user/User.java b/src/main/java/com/aolda/itda/entity/user/User.java
new file mode 100644
index 0000000..df006b2
--- /dev/null
+++ b/src/main/java/com/aolda/itda/entity/user/User.java
@@ -0,0 +1,24 @@
+package com.aolda.itda.entity.user;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "user")
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class User {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(nullable = false)
+    private Long userId;
+
+    private String keystoneUsername;
+    private String keystoneId;
+}
-- 
GitLab


From e92e5bd4765a609fe9ac63b6d8d59f72b5e7ddb1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Mon, 10 Mar 2025 15:35:31 +0900
Subject: [PATCH 11/41] =?UTF-8?q?feat:=20=ED=8F=AC=ED=8A=B8=ED=8F=AC?=
 =?UTF-8?q?=EC=9B=8C=EB=94=A9,=20=EC=98=B5=EC=85=98=20=ED=85=9C=ED=94=8C?=
 =?UTF-8?q?=EB=A6=BF=20=EC=9E=91=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../itda/template/ForwardingTemplate.java     | 13 ++++++++++
 .../aolda/itda/template/OptionTemplate.java   | 26 +++++++++++++++++++
 2 files changed, 39 insertions(+)
 create mode 100644 src/main/java/com/aolda/itda/template/ForwardingTemplate.java
 create mode 100644 src/main/java/com/aolda/itda/template/OptionTemplate.java

diff --git a/src/main/java/com/aolda/itda/template/ForwardingTemplate.java b/src/main/java/com/aolda/itda/template/ForwardingTemplate.java
new file mode 100644
index 0000000..8031bff
--- /dev/null
+++ b/src/main/java/com/aolda/itda/template/ForwardingTemplate.java
@@ -0,0 +1,13 @@
+package com.aolda.itda.template;
+
+import org.springframework.stereotype.Component;
+
+@Component
+public class ForwardingTemplate {
+
+    public String getPortForwardingWithTCP(String instanceIp, String serverPort) {
+        return "\nlisten " + serverPort + "; \n" +
+                "listen [::]:" + serverPort + "; \n" +
+                "proxy_pass " + instanceIp + ";\n";
+    }
+}
diff --git a/src/main/java/com/aolda/itda/template/OptionTemplate.java b/src/main/java/com/aolda/itda/template/OptionTemplate.java
new file mode 100644
index 0000000..3e1e92c
--- /dev/null
+++ b/src/main/java/com/aolda/itda/template/OptionTemplate.java
@@ -0,0 +1,26 @@
+package com.aolda.itda.template;
+
+import org.springframework.stereotype.Component;
+
+@Component
+public class OptionTemplate {
+
+    public String getSSL(Long certificateId) {
+        return "\nconf.d/include/letsencrypt-acme-challenge.conf;\n" +
+                "include conf.d/include/ssl-ciphers.conf;\n" +
+                "ssl_certificate /etc/letsencrypt/live/npm-" + certificateId + "/fullchain.pem;\n" +
+                "ssl_certificate_key /etc/letsencrypt/live/npm-" + certificateId + "/privkey.pem;\n";
+    }
+
+    public String getAssetCaching() {
+        return "include conf.d/include/assets.conf;\n";
+    }
+
+    public String getBlockExploits() {
+        return "include conf.d/include/block-exploits.conf;\n";
+    }
+
+    public String getForceSSL() {
+        return "include conf.d/include/force-ssl.conf;\n";
+    }
+}
-- 
GitLab


From b96e7708b01a98ca6ae80dfa905afe7014728d70 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Fri, 14 Mar 2025 22:16:11 +0900
Subject: [PATCH 12/41] =?UTF-8?q?=ED=8F=AC=ED=8A=B8=ED=8F=AC=EC=9B=8C?=
 =?UTF-8?q?=EB=94=A9=20CRUD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 build.gradle                                  |   1 +
 .../aolda/itda/config/AuthInterceptor.java    |  37 ++++
 .../java/com/aolda/itda/config/WebConfig.java |  15 ++
 .../forwarding/ForwardingController.java      |  46 +++++
 .../java/com/aolda/itda/dto/PageResp.java     |  27 +++
 .../itda/dto/forwarding/ForwardingDTO.java    |  45 +++++
 .../itda/entity/certificate/Certificate.java  |   4 +-
 .../itda/entity/forwarding/Forwarding.java    |  32 ++++
 .../aolda/itda/entity/routing/Routing.java    |   3 +-
 .../com/aolda/itda/exception/ErrorCode.java   |   9 +-
 .../forwarding/ForwardingRepository.java      |  14 ++
 .../com/aolda/itda/service/AuthService.java   |  17 +-
 .../service/forwarding/ForwardingService.java | 158 ++++++++++++++++++
 .../itda/template/ForwardingTemplate.java     |  11 +-
 14 files changed, 402 insertions(+), 17 deletions(-)
 create mode 100644 src/main/java/com/aolda/itda/config/AuthInterceptor.java
 create mode 100644 src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java
 create mode 100644 src/main/java/com/aolda/itda/dto/PageResp.java
 create mode 100644 src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java
 create mode 100644 src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java
 create mode 100644 src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java

diff --git a/build.gradle b/build.gradle
index 5cef9f6..b88f454 100644
--- a/build.gradle
+++ b/build.gradle
@@ -26,6 +26,7 @@ repositories {
 dependencies {
     implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
     implementation 'org.springframework.boot:spring-boot-starter-web'
+    implementation 'org.springframework.boot:spring-boot-starter-validation'
     compileOnly 'org.projectlombok:lombok'
     runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
     runtimeOnly 'com.mysql:mysql-connector-j'
diff --git a/src/main/java/com/aolda/itda/config/AuthInterceptor.java b/src/main/java/com/aolda/itda/config/AuthInterceptor.java
new file mode 100644
index 0000000..63f2320
--- /dev/null
+++ b/src/main/java/com/aolda/itda/config/AuthInterceptor.java
@@ -0,0 +1,37 @@
+package com.aolda.itda.config;
+
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.service.AuthService;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+@RequiredArgsConstructor
+@Component
+@Slf4j
+public class AuthInterceptor implements HandlerInterceptor {
+
+    private final AuthService authService;
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        String token = request.getHeader("X-Subject-Token");
+        if (token == null || token.isEmpty()) {
+            throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI());
+        }
+        // 어드민과 일반 유저 구분 필요
+        try {
+            if (authService.validateTokenAndGetUserId(token) != null) {
+                return true;
+            }
+        } catch (Exception e) {
+            log.error("Token validation failed for URI {}: {}", request.getRequestURI(), e.getMessage(), e);
+            throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI());
+        }
+        throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI());
+    }
+}
diff --git a/src/main/java/com/aolda/itda/config/WebConfig.java b/src/main/java/com/aolda/itda/config/WebConfig.java
index 9eb992b..fd00807 100644
--- a/src/main/java/com/aolda/itda/config/WebConfig.java
+++ b/src/main/java/com/aolda/itda/config/WebConfig.java
@@ -1,11 +1,17 @@
 package com.aolda.itda.config;
 
+import lombok.RequiredArgsConstructor;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
 @Configuration
+@RequiredArgsConstructor
 public class WebConfig implements WebMvcConfigurer {
+
+    private final AuthInterceptor authInterceptor;
+
     @Override
     public void addCorsMappings(CorsRegistry registry) { // 스프링단에서 cors 설정
         registry.addMapping("/**")
@@ -16,4 +22,13 @@ public class WebConfig implements WebMvcConfigurer {
                 .exposedHeaders("Authorization", "X-Refresh-Token", "Access-Control-Allow-Origin")
         ;
     }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        String[] excludeAuth = {"/error", "/api/auth/*" };
+
+        registry.addInterceptor(authInterceptor)
+                .addPathPatterns("/**")
+                .excludePathPatterns(excludeAuth);
+    }
 }
diff --git a/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java
new file mode 100644
index 0000000..19786bd
--- /dev/null
+++ b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java
@@ -0,0 +1,46 @@
+package com.aolda.itda.controller.forwarding;
+
+import com.aolda.itda.dto.forwarding.ForwardingDTO;
+import com.aolda.itda.service.forwarding.ForwardingService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api")
+@RequiredArgsConstructor
+public class ForwardingController {
+
+    private final ForwardingService forwardingService;
+
+    @PostMapping("/forwarding")
+    public ResponseEntity<Object> create(@RequestParam String projectId,
+                                         @RequestBody ForwardingDTO dto) {
+        forwardingService.createForwarding(projectId, dto);
+        return ResponseEntity.ok().build();
+    }
+
+    @GetMapping("/forwarding")
+    public ResponseEntity<Object> view(@RequestParam Long forwardingId) {
+        return ResponseEntity.ok(forwardingService.getForwarding(forwardingId));
+    }
+
+    @GetMapping("/forwardings")
+    public ResponseEntity<Object> lists(@RequestParam String projectId) {
+        return ResponseEntity.ok(forwardingService.getForwardings(projectId));
+    }
+
+    @PatchMapping("/forwarding")
+    public ResponseEntity<Object> edit(@RequestParam Long forwardingId,
+                                         @RequestBody ForwardingDTO dto) {
+        forwardingService.editForwarding(forwardingId, dto);
+        return ResponseEntity.ok().build();
+    }
+
+    @DeleteMapping("/forwarding")
+    public ResponseEntity<Object> delete(@RequestParam Long forwardingId) {
+        forwardingService.deleteForwarding(forwardingId);
+        return ResponseEntity.ok().build();
+    }
+
+}
diff --git a/src/main/java/com/aolda/itda/dto/PageResp.java b/src/main/java/com/aolda/itda/dto/PageResp.java
new file mode 100644
index 0000000..49a2a6c
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/PageResp.java
@@ -0,0 +1,27 @@
+package com.aolda.itda.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Builder
+@Getter
+@AllArgsConstructor
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class PageResp<T> {
+    private Integer totalPages;
+    private Integer totalElements;
+    private Integer size;
+    private List<T> contents;
+    private Boolean first;
+    private Boolean last;
+
+    public static <T> PageRespBuilder<T> builderFor(Class<T> clazz) {
+        return (PageRespBuilder<T>) new PageRespBuilder<>();
+    }
+}
diff --git a/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java b/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java
new file mode 100644
index 0000000..3302d0a
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java
@@ -0,0 +1,45 @@
+package com.aolda.itda.dto.forwarding;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class ForwardingDTO {
+
+    private Long id;
+
+    @Pattern(regexp = "^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$",
+            message = "잘못된 IP 형식 (server)")
+    private String serverIp;
+
+    @NotBlank(message = "serverPort 값이 존재하지 않습니다")
+    @Pattern(regexp = "^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$",
+            message = "잘못된 포트 형식 (server)")
+    private String serverPort;
+
+    @NotBlank(message = "instanceIp 값이 존재하지 않습니다")
+    @Pattern(regexp = "^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$",
+            message = "잘못된 IP 형식 (instance)")
+    private String instanceIp;
+
+    @NotBlank(message = "instancePort 값이 존재하지 않습니다")
+    @Pattern(regexp = "^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$",
+            message = "잘못된 포트 형식 (instance)")
+    private String instancePort;
+
+    @NotBlank(message = "name 값이 존재하지 않습니다")
+    private String name;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+}
diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
index 76253a1..5ab56a9 100644
--- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
+++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
@@ -11,7 +11,7 @@ import lombok.NoArgsConstructor;
 import java.time.LocalDateTime;
 
 @Entity
-@Table(name = "routing")
+@Table(name = "certificate")
 @Getter
 @Builder
 @NoArgsConstructor
@@ -40,7 +40,7 @@ public class Certificate extends BaseTimeEntity {
 
     private Boolean isDeleted;
 
-    private String metadata;
+    private String description;
 
 
 }
diff --git a/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java
index 0288af8..b0c8d34 100644
--- a/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java
+++ b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java
@@ -1,5 +1,6 @@
 package com.aolda.itda.entity.forwarding;
 
+import com.aolda.itda.dto.forwarding.ForwardingDTO;
 import com.aolda.itda.entity.BaseTimeEntity;
 import com.aolda.itda.entity.user.User;
 import jakarta.persistence.*;
@@ -36,4 +37,35 @@ public class Forwarding extends BaseTimeEntity {
     private String instancePort;
 
     private Boolean isDeleted;
+
+    private String name;
+
+    public ForwardingDTO toForwardingDTO() {
+        return ForwardingDTO.builder()
+                .id(forwardingId)
+                .name(name)
+                .serverPort(serverPort)
+                .instanceIp(instanceIp)
+                .instancePort(instancePort)
+                .createdAt(getCreatedAt())
+                .updatedAt(getUpdatedAt())
+                .build();
+    }
+
+    public void edit(ForwardingDTO dto) {
+        this.name = dto.getName() != null ? dto.getName() : this.name;
+        this.serverPort = dto.getServerPort() != null &&
+                dto.getServerPort().matches("^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$")
+                ? dto.getServerPort() : this.serverPort;
+        this.instanceIp = dto.getInstanceIp() != null &&
+                dto.getInstanceIp().matches("^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$")
+                ? dto.getInstanceIp() : this.instanceIp;
+        this.instancePort = dto.getInstancePort() != null &&
+                dto.getInstancePort().matches("^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$")
+                ? dto.getInstancePort() : this.instancePort;
+    }
+
+    public void delete() {
+        this.isDeleted = true;
+    }
 }
diff --git a/src/main/java/com/aolda/itda/entity/routing/Routing.java b/src/main/java/com/aolda/itda/entity/routing/Routing.java
index 2eb9bd1..0c6b2aa 100644
--- a/src/main/java/com/aolda/itda/entity/routing/Routing.java
+++ b/src/main/java/com/aolda/itda/entity/routing/Routing.java
@@ -27,7 +27,7 @@ public class Routing extends BaseTimeEntity {
     private User user;
 
     @OneToOne
-    @JoinColumn(name = "certificate_id")
+    @JoinColumn(name = "certificate_id", nullable = false)
     private Certificate certificate;
 
     private String projectId;
@@ -38,5 +38,6 @@ public class Routing extends BaseTimeEntity {
 
     private Boolean isDeleted;
 
+    private String description;
 
 }
diff --git a/src/main/java/com/aolda/itda/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java
index 0916e71..c36e7f4 100644
--- a/src/main/java/com/aolda/itda/exception/ErrorCode.java
+++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java
@@ -12,7 +12,14 @@ public enum ErrorCode {
     INVALID_USER_INFO(HttpStatus.BAD_REQUEST, "잘못된 회원 정보입니다"),
 
     // Token
-    INVALID_TOKEN(HttpStatus.BAD_REQUEST, "잘못된 토큰입니다");
+    INVALID_TOKEN(HttpStatus.BAD_REQUEST, "잘못된 토큰입니다"),
+
+    //Forwarding
+    FAIL_CREATE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 생성하지 못했습니다"),
+    NOT_FOUND_FORWARDING(HttpStatus.BAD_REQUEST, "포트포워딩 파일이 존재하지 않습니다"),
+    INVALID_CONF_INPUT(HttpStatus.BAD_REQUEST, "잘못된 입력이 존재합니다"),
+    DUPLICATED_INSTANCE_INFO(HttpStatus.BAD_REQUEST, "중복된 인스턴스 IP와 포트입니다"),
+    DUPLICATED_SERVER_PORT(HttpStatus.BAD_REQUEST, "중복된 서버 포트입니다");
 
     private final HttpStatus status;
     private final String message;
diff --git a/src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java b/src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java
new file mode 100644
index 0000000..c461762
--- /dev/null
+++ b/src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java
@@ -0,0 +1,14 @@
+package com.aolda.itda.repository.forwarding;
+
+import com.aolda.itda.entity.forwarding.Forwarding;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface ForwardingRepository extends JpaRepository<Forwarding, Long> {
+    List<Forwarding> findByProjectIdAndIsDeleted(String projectId, Boolean isDeleted);
+    Optional<Forwarding> findByForwardingIdAndIsDeleted(Long forwardingId, Boolean isDeleted);
+    Boolean existsByInstanceIpAndInstancePortAndIsDeleted(String instanceIp, String instancePort, Boolean isDeleted);
+    Boolean existsByServerPortAndIsDeleted(String serverPort, Boolean isDeleted);
+}
diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java
index 1dea9a2..cc523fa 100644
--- a/src/main/java/com/aolda/itda/service/AuthService.java
+++ b/src/main/java/com/aolda/itda/service/AuthService.java
@@ -15,7 +15,6 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.*;
 import org.springframework.stereotype.Service;
 import org.springframework.web.client.HttpClientErrorException;
-import org.springframework.web.client.RestClientException;
 import org.springframework.web.client.RestTemplate;
 
 import java.util.*;
@@ -79,8 +78,13 @@ public class AuthService {
                 "}";
 
         HttpEntity<String> requestEntity = new HttpEntity<>(requestBody, headers);
-        ResponseEntity<Map> res = restTemplate.postForEntity(url, requestEntity, Map.class);
-
+        ResponseEntity<Map> res;
+        try {
+            res = restTemplate.postForEntity(url, requestEntity, Map.class);
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new CustomException(ErrorCode.INVALID_USER_INFO);
+        }
         Map<String, Object> resToken = (Map<String, Object>) res.getBody().get("token");
         Map<String, Object> resUser = (Map<String, Object>) resToken.get("user");
         String userId = (String) resUser.get("id");
@@ -263,7 +267,7 @@ public class AuthService {
         return lists;
     }
 
-    private String validateTokenAndGetUserId(String token) throws JsonProcessingException {
+    public String validateTokenAndGetUserId(String token) throws JsonProcessingException {
         String url = keystone + "/auth/tokens";
         HttpHeaders headers = new HttpHeaders();
         headers.set("X-Auth-Token", token);
@@ -273,7 +277,6 @@ public class AuthService {
         try {
             res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
         } catch (HttpClientErrorException.NotFound e) {
-            System.out.println("validate");
             throw new CustomException(ErrorCode.INVALID_TOKEN);
         }
         return objectMapper.readTree(res.getBody()).path("token").path("user").path("id").asText();
@@ -290,18 +293,14 @@ public class AuthService {
             res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
         } catch (RuntimeException e) {
             e.printStackTrace();
-            System.out.println("runtime");
             return false;
         }
         JsonNode node = objectMapper.readTree(res.getBody()).path("role_assignments");
         String system_all = node.path("scope").path("system").path("all").asText();
         String role = node.path("role").path("name").asText();
-        System.out.println("role: " + role);
         if (system_all.equals("true") && role.equals("admin")) {
-            System.out.println(system_all);
             return true;
         }
-        System.out.println("hi");
         return false;
 
     }
diff --git a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
new file mode 100644
index 0000000..22a1aee
--- /dev/null
+++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
@@ -0,0 +1,158 @@
+package com.aolda.itda.service.forwarding;
+
+import com.aolda.itda.dto.PageResp;
+import com.aolda.itda.dto.forwarding.ForwardingDTO;
+import com.aolda.itda.entity.forwarding.Forwarding;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.repository.forwarding.ForwardingRepository;
+import com.aolda.itda.template.ForwardingTemplate;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validation;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class ForwardingService {
+
+    @Value("${spring.server.base-ip}")
+    private String serverBaseIp;
+    private final ForwardingTemplate forwardingTemplate;
+    private final ForwardingRepository forwardingRepository;
+
+    /* 포트포워딩 정보 조회 */
+    public ForwardingDTO getForwarding(Long forwardingId) {
+        Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
+        return forwarding.toForwardingDTO();
+    }
+
+    /* 포트포워딩 목록 조회 */
+    public PageResp<ForwardingDTO> getForwardings(String projectId) {
+
+        return PageResp.<ForwardingDTO>builder()
+                .contents(forwardingRepository.findByProjectIdAndIsDeleted(projectId, false)
+                        .stream()
+                        .map(Forwarding::toForwardingDTO)
+                        .toList()).build();
+    }
+
+    /* 포트포워딩 생성 */
+    public void createForwarding(String projectId, ForwardingDTO dto) {
+
+        /* 입력 DTO 검증 */
+        validateDTO(dto);
+
+        /* 중복 검증 */
+        if (forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted(dto.getInstanceIp(), dto.getInstancePort(), false)) {
+            throw new CustomException(ErrorCode.DUPLICATED_INSTANCE_INFO);
+        }
+
+        if (forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) {
+            throw new CustomException(ErrorCode.DUPLICATED_SERVER_PORT);
+        }
+
+        /* 포트포워딩 엔티티 생성 */
+        Forwarding forwarding = Forwarding.builder()
+                .isDeleted(false)
+                .projectId(projectId)
+                .name(dto.getName())
+                .serverIp(dto.getServerIp() == null ? serverBaseIp : dto.getServerIp())
+                .serverPort(dto.getServerPort())
+                .instanceIp(dto.getInstanceIp())
+                .instancePort(dto.getInstancePort())
+                .build();
+
+        forwardingRepository.save(forwarding);
+
+        /* nginx conf 파일 생성 및 예외 처리 */
+        String content = forwardingTemplate.getPortForwardingWithTCP(dto.getServerPort(), dto.getInstanceIp(), dto.getInstancePort(), dto.getName());
+        String confPath = "/data/nginx/stream/" + forwarding.getForwardingId() + ".conf";
+
+        File file = new File(confPath);
+        try {
+            Path path = Paths.get(confPath);
+            Files.createDirectories(path.getParent());
+            if (!file.createNewFile()) {
+                throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "중복된 포트포워딩 Conf 파일이 존재합니다");
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new CustomException(ErrorCode.FAIL_CREATE_CONF);
+        }
+
+        /* conf 파일 작성 및 예외 처리 */
+        try {
+            BufferedWriter bw = new BufferedWriter(new FileWriter(file, true)); // 예외처리 필요
+            bw.write(content);
+            bw.flush();
+            bw.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+            if (file.exists()) {
+                file.delete();
+            }
+            throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "포트포워딩 Conf 파일을 작성하지 못했습니다");
+        }
+
+
+    }
+
+    /* 포트포워딩 정보 수정 */
+    public void editForwarding(Long forwardingId, ForwardingDTO dto) {
+        Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
+
+        /* 중복 검증 */
+        if (forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted(dto.getInstanceIp(), dto.getInstancePort(), false)) {
+            throw new CustomException(ErrorCode.DUPLICATED_INSTANCE_INFO);
+        }
+
+        if (forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) {
+            throw new CustomException(ErrorCode.DUPLICATED_SERVER_PORT);
+        }
+
+        /* 정보 수정 */
+        forwarding.edit(dto);
+        forwardingRepository.save(forwarding);
+    }
+
+    /* 포트포워딩 삭제 (소프트) */
+    public void deleteForwarding(Long forwardingId) {
+        Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
+
+        /* 파일 삭제 */
+        String confPath = "/data/nginx/stream/" + forwarding.getForwardingId() + ".conf";
+        File file = new File(confPath);
+        if (!file.delete()) {
+            throw new CustomException(ErrorCode.NOT_FOUND_FORWARDING, "Conf 파일이 존재하지 않아 삭제할 수 없습니다");
+        }
+
+        /* DB */
+        forwarding.delete();
+        forwardingRepository.save(forwarding);
+
+    }
+
+    /* 입력 DTO 검증 */
+    private void validateDTO(ForwardingDTO dto) {
+
+        for (ConstraintViolation<ForwardingDTO> violation : Validation.buildDefaultValidatorFactory().getValidator().validate(dto)) {
+            throw new CustomException(ErrorCode.INVALID_CONF_INPUT, violation.getMessage());
+        }
+
+    }
+}
diff --git a/src/main/java/com/aolda/itda/template/ForwardingTemplate.java b/src/main/java/com/aolda/itda/template/ForwardingTemplate.java
index 8031bff..6d8bf2c 100644
--- a/src/main/java/com/aolda/itda/template/ForwardingTemplate.java
+++ b/src/main/java/com/aolda/itda/template/ForwardingTemplate.java
@@ -5,9 +5,12 @@ import org.springframework.stereotype.Component;
 @Component
 public class ForwardingTemplate {
 
-    public String getPortForwardingWithTCP(String instanceIp, String serverPort) {
-        return "\nlisten " + serverPort + "; \n" +
-                "listen [::]:" + serverPort + "; \n" +
-                "proxy_pass " + instanceIp + ";\n";
+    public String getPortForwardingWithTCP(String serverPort, String instanceIp, String instancePort, String name) {
+        return  "# " + name + "\n" +
+                "server { \n" +
+                " listen " + serverPort + "; \n" +
+                " listen [::]:" + serverPort + "; \n" +
+                " proxy_pass " + instanceIp + ":" + instancePort + ";\n" +
+                "} \n";
     }
 }
-- 
GitLab


From e6f1aafb3915c7623458f0e9d43eeca3908ce317 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Fri, 14 Mar 2025 23:27:46 +0900
Subject: [PATCH 13/41] =?UTF-8?q?fix:=20=ED=8F=AC=ED=8A=B8=ED=8F=AC?=
 =?UTF-8?q?=EC=9B=8C=EB=94=A9=20=ED=8C=8C=EC=9D=BC=EC=9D=84=20=EC=88=98?=
 =?UTF-8?q?=EC=A0=95=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8D=98=20=EB=AC=B8?=
 =?UTF-8?q?=EC=A0=9C,=20=EC=83=9D=EC=84=B1=EC=9D=BC/=EC=88=98=EC=A0=95?=
 =?UTF-8?q?=EC=9D=BC=20=ED=95=84=EB=93=9C=EA=B0=80=20=EC=A0=9C=EB=8C=80?=
 =?UTF-8?q?=EB=A1=9C=20=EC=A0=81=EC=9A=A9=EB=90=98=EC=A7=80=20=EC=95=8A?=
 =?UTF-8?q?=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/aolda/itda/ItdaApplication.java  |  2 ++
 .../com/aolda/itda/exception/ErrorCode.java   |  1 +
 .../service/forwarding/ForwardingService.java | 33 ++++++++++++++++---
 3 files changed, 32 insertions(+), 4 deletions(-)

diff --git a/src/main/java/com/aolda/itda/ItdaApplication.java b/src/main/java/com/aolda/itda/ItdaApplication.java
index c7f4ef7..674e657 100644
--- a/src/main/java/com/aolda/itda/ItdaApplication.java
+++ b/src/main/java/com/aolda/itda/ItdaApplication.java
@@ -2,8 +2,10 @@ package com.aolda.itda;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
 
 @SpringBootApplication
+@EnableJpaAuditing
 public class ItdaApplication {
 
     public static void main(String[] args) {
diff --git a/src/main/java/com/aolda/itda/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java
index c36e7f4..2fc313b 100644
--- a/src/main/java/com/aolda/itda/exception/ErrorCode.java
+++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java
@@ -16,6 +16,7 @@ public enum ErrorCode {
 
     //Forwarding
     FAIL_CREATE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 생성하지 못했습니다"),
+    FAIL_UPDATE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 수정하지 못했습니다"),
     NOT_FOUND_FORWARDING(HttpStatus.BAD_REQUEST, "포트포워딩 파일이 존재하지 않습니다"),
     INVALID_CONF_INPUT(HttpStatus.BAD_REQUEST, "잘못된 입력이 존재합니다"),
     DUPLICATED_INSTANCE_INFO(HttpStatus.BAD_REQUEST, "중복된 인스턴스 IP와 포트입니다"),
diff --git a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
index 22a1aee..e47001d 100644
--- a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
+++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
@@ -115,17 +115,42 @@ public class ForwardingService {
         Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false)
                 .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
 
+        forwarding.edit(dto);
+
         /* 중복 검증 */
-        if (forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted(dto.getInstanceIp(), dto.getInstancePort(), false)) {
+        if (!(dto.getInstanceIp() == null && dto.getInstancePort() == null) &&
+                forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted(forwarding.getInstanceIp()
+                , forwarding.getInstancePort()
+                , false)) {
             throw new CustomException(ErrorCode.DUPLICATED_INSTANCE_INFO);
         }
 
-        if (forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) {
+        if (dto.getServerPort() != null && forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) {
             throw new CustomException(ErrorCode.DUPLICATED_SERVER_PORT);
         }
 
-        /* 정보 수정 */
-        forwarding.edit(dto);
+        /* 파일 수정 */
+        String content = forwardingTemplate.getPortForwardingWithTCP(forwarding.getServerPort(),
+                forwarding.getInstanceIp(),
+                forwarding.getInstancePort(),
+                forwarding.getName());
+        String confPath = "/data/nginx/stream/" + forwarding.getForwardingId() + ".conf";
+        File file = new File(confPath);
+        if (!file.exists()) {
+            throw new CustomException(ErrorCode.NOT_FOUND_FORWARDING, "Conf 파일이 존재하지 않아 수정할 수 없습니다");
+        }
+
+        try {
+            BufferedWriter bw = new BufferedWriter(new FileWriter(file, true)); // 예외처리 필요
+            bw.write(content);
+            bw.flush();
+            bw.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "포트포워딩 Conf 파일을 수정하지 못했습니다");
+        }
+
+        /* DB 정보 수정 */
         forwardingRepository.save(forwarding);
     }
 
-- 
GitLab


From 5b1a3a59c2ef7436055ea4f46eb1ad14f3c034c3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Sat, 15 Mar 2025 11:31:04 +0900
Subject: [PATCH 14/41] =?UTF-8?q?feat:=20Dockerfile=20=EC=9E=91=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 Dockerfile | 10 ++++++++++
 1 file changed, 10 insertions(+)
 create mode 100644 Dockerfile

diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..16f6160
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,10 @@
+FROM gradle:jdk21 AS build
+WORKDIR /home/gradle/project
+COPY --chown=gradle:gradle . .
+RUN gradle clean bootJar --no-daemon
+
+FROM openjdk:21-jdk-slim
+WORKDIR /app
+COPY --from=build /home/gradle/project/build/libs/*.jar app.jar
+EXPOSE 8080
+ENTRYPOINT ["java", "-jar", "app.jar"]
-- 
GitLab


From c0d0a410f493f4bffe0c7d6cb4ca33e9f75a3258 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Sat, 15 Mar 2025 12:28:51 +0900
Subject: [PATCH 15/41] =?UTF-8?q?fix:=20conf=20=EC=88=98=EC=A0=95=EC=9D=B4?=
 =?UTF-8?q?=20=EB=8D=AE=EC=96=B4=EC=93=B0=EA=B8=B0=20=EB=90=98=EB=8D=98=20?=
 =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/aolda/itda/service/forwarding/ForwardingService.java    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
index e47001d..f1d2e46 100644
--- a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
+++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
@@ -95,7 +95,7 @@ public class ForwardingService {
 
         /* conf 파일 작성 및 예외 처리 */
         try {
-            BufferedWriter bw = new BufferedWriter(new FileWriter(file, true)); // 예외처리 필요
+            BufferedWriter bw = new BufferedWriter(new FileWriter(file, false)); // 예외처리 필요
             bw.write(content);
             bw.flush();
             bw.close();
-- 
GitLab


From f78d7851ea18441b994dddc97b3f4acd31546865 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Sat, 15 Mar 2025 16:26:46 +0900
Subject: [PATCH 16/41] =?UTF-8?q?feat:=20Nginx=20=ED=85=8C=EC=8A=A4?=
 =?UTF-8?q?=ED=8A=B8,=20=EB=A6=AC=EB=A1=9C=EB=93=9C=20=EB=A1=9C=EC=A7=81?=
 =?UTF-8?q?=20=EC=B6=94=EA=B0=80,=20conf=20=ED=8C=8C=EC=9D=BC=20=EA=B4=80?=
 =?UTF-8?q?=EB=A0=A8=20=EB=A1=A4=EB=B0=B1=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?=
 =?UTF-8?q?=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/aolda/itda/exception/ErrorCode.java   |  11 +-
 .../service/forwarding/ForwardingService.java | 163 +++++++++++++++++-
 2 files changed, 166 insertions(+), 8 deletions(-)

diff --git a/src/main/java/com/aolda/itda/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java
index 2fc313b..443f3a1 100644
--- a/src/main/java/com/aolda/itda/exception/ErrorCode.java
+++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java
@@ -14,13 +14,20 @@ public enum ErrorCode {
     // Token
     INVALID_TOKEN(HttpStatus.BAD_REQUEST, "잘못된 토큰입니다"),
 
-    //Forwarding
+    // Forwarding
     FAIL_CREATE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 생성하지 못했습니다"),
     FAIL_UPDATE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 수정하지 못했습니다"),
     NOT_FOUND_FORWARDING(HttpStatus.BAD_REQUEST, "포트포워딩 파일이 존재하지 않습니다"),
     INVALID_CONF_INPUT(HttpStatus.BAD_REQUEST, "잘못된 입력이 존재합니다"),
     DUPLICATED_INSTANCE_INFO(HttpStatus.BAD_REQUEST, "중복된 인스턴스 IP와 포트입니다"),
-    DUPLICATED_SERVER_PORT(HttpStatus.BAD_REQUEST, "중복된 서버 포트입니다");
+    DUPLICATED_SERVER_PORT(HttpStatus.BAD_REQUEST, "중복된 서버 포트입니다"),
+
+    // Nginx
+    FAIL_NGINX_CONF_TEST(HttpStatus.BAD_REQUEST, "Conf 파일 테스트에 실패했습니다"),
+    FAIL_NGINX_CONF_RELOAD(HttpStatus.BAD_REQUEST, "Nginx 재시작에 실패했습니다"),
+
+    FAIL_DELETE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 삭제하지 못했습니다"),
+    FAIL_ROLL_BACK(HttpStatus.BAD_REQUEST, "롤백 실패");
 
     private final HttpStatus status;
     private final String message;
diff --git a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
index f1d2e46..4bfbce4 100644
--- a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
+++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
@@ -7,12 +7,20 @@ import com.aolda.itda.exception.CustomException;
 import com.aolda.itda.exception.ErrorCode;
 import com.aolda.itda.repository.forwarding.ForwardingRepository;
 import com.aolda.itda.template.ForwardingTemplate;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import jakarta.validation.ConstraintViolation;
 import jakarta.validation.Validation;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.HttpServerErrorException;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestTemplate;
 
 import java.io.BufferedWriter;
 import java.io.File;
@@ -21,16 +29,20 @@ import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.Map;
 
 @Service
 @Transactional
 @RequiredArgsConstructor
+@Slf4j
 public class ForwardingService {
 
     @Value("${spring.server.base-ip}")
     private String serverBaseIp;
     private final ForwardingTemplate forwardingTemplate;
     private final ForwardingRepository forwardingRepository;
+    private final RestTemplate restTemplate = new RestTemplate();
 
     /* 포트포워딩 정보 조회 */
     public ForwardingDTO getForwarding(Long forwardingId) {
@@ -101,12 +113,47 @@ public class ForwardingService {
             bw.close();
         } catch (Exception e) {
             e.printStackTrace();
-            if (file.exists()) {
-                file.delete();
+            if (file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_DELETE_CONF);
             }
             throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "포트포워딩 Conf 파일을 작성하지 못했습니다");
         }
 
+        /* nginx test */
+        String url = "http://nginx:8081/nginx-api/test";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (HttpServerErrorException.InternalServerError e) {
+            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
+            if (file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        } catch (Exception e) {
+            log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage());
+            if (file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        }
+
+        /* nginx reload */
+        url = "http://nginx:8081/nginx-api/reload";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (HttpServerErrorException.InternalServerError e) {
+            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
+            if (file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
+        } catch (Exception e) {
+            log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage());
+            if (file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
+        }
 
     }
 
@@ -140,8 +187,13 @@ public class ForwardingService {
             throw new CustomException(ErrorCode.NOT_FOUND_FORWARDING, "Conf 파일이 존재하지 않아 수정할 수 없습니다");
         }
 
+        Path backup;
         try {
-            BufferedWriter bw = new BufferedWriter(new FileWriter(file, true)); // 예외처리 필요
+            backup = Files.createTempFile("temp_", ".tmp");
+            Files.copy(Paths.get(confPath), backup, StandardCopyOption.REPLACE_EXISTING
+            , StandardCopyOption.COPY_ATTRIBUTES);
+
+            BufferedWriter bw = new BufferedWriter(new FileWriter(file, false));
             bw.write(content);
             bw.flush();
             bw.close();
@@ -150,6 +202,59 @@ public class ForwardingService {
             throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "포트포워딩 Conf 파일을 수정하지 못했습니다");
         }
 
+        /* nginx test */
+        String url = "http://nginx:8081/nginx-api/test";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (HttpServerErrorException.InternalServerError e) {
+            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
+            try {
+                Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
+                        , StandardCopyOption.COPY_ATTRIBUTES);
+                Files.delete(backup);
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(포트포워딩 Conf 파일 수정)");
+            }
+
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        } catch (RuntimeException e) {
+            log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage());
+            try {
+                Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
+                        , StandardCopyOption.COPY_ATTRIBUTES);
+                Files.delete(backup);
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(포트포워딩 Conf 파일 수정)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        }
+
+        /* nginx reload */
+        url = "http://nginx:8081/nginx-api/reload";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (HttpServerErrorException.InternalServerError e) {
+            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
+            try {
+                Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
+                        , StandardCopyOption.COPY_ATTRIBUTES);
+                Files.delete(backup);
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(포트포워딩 Conf 파일 수정)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
+        } catch (RuntimeException e) {
+            log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage());
+            try {
+                Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
+                , StandardCopyOption.COPY_ATTRIBUTES);
+                Files.delete(backup);
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(포트포워딩 Conf 파일 수정)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
+        }
+
         /* DB 정보 수정 */
         forwardingRepository.save(forwarding);
     }
@@ -161,9 +266,55 @@ public class ForwardingService {
 
         /* 파일 삭제 */
         String confPath = "/data/nginx/stream/" + forwarding.getForwardingId() + ".conf";
-        File file = new File(confPath);
-        if (!file.delete()) {
-            throw new CustomException(ErrorCode.NOT_FOUND_FORWARDING, "Conf 파일이 존재하지 않아 삭제할 수 없습니다");
+        String deletePath = confPath + ".deleted";
+        try {
+            Files.move(Paths.get(confPath), Paths.get(deletePath));
+        } catch (IOException e) {
+            throw new CustomException(ErrorCode.FAIL_DELETE_CONF);
+        }
+
+        /* nginx test */
+        String url = "http://nginx:8081/nginx-api/test";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (HttpServerErrorException.InternalServerError e) {
+            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
+            try {
+                Files.move(Paths.get(deletePath), Paths.get(confPath));
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(포트포워딩 Conf 삭제)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        } catch (Exception e) {
+            log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage());
+            try {
+                Files.move(Paths.get(deletePath), Paths.get(confPath));
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(포트포워딩 Conf 삭제)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        }
+
+        /* nginx reload */
+        url = "http://nginx:8081/nginx-api/reload";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (HttpServerErrorException.InternalServerError e) {
+            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
+            try {
+                Files.move(Paths.get(deletePath), Paths.get(confPath));
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(포트포워딩 Conf 삭제)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
+        } catch (Exception e) {
+            log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage());
+            try {
+                Files.move(Paths.get(deletePath), Paths.get(confPath));
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(포트포워딩 Conf 삭제)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
         }
 
         /* DB */
-- 
GitLab


From 82fd3ae9115c9400f203c7be34d5cbb761823b64 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Sat, 15 Mar 2025 19:35:32 +0900
Subject: [PATCH 17/41] =?UTF-8?q?fix:=20conf=20=EC=A4=91=EB=B3=B5=20?=
 =?UTF-8?q?=EA=B2=80=EC=A6=9D=EC=9D=B4=20=EC=95=88=EB=90=98=EB=8D=98=20?=
 =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../service/forwarding/ForwardingService.java | 24 +++++++++++++------
 1 file changed, 17 insertions(+), 7 deletions(-)

diff --git a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
index 4bfbce4..9ec9343 100644
--- a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
+++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
@@ -162,21 +162,31 @@ public class ForwardingService {
         Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false)
                 .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
 
-        forwarding.edit(dto);
 
         /* 중복 검증 */
+        if (dto.getServerPort() != null && forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) {
+            System.out.println(dto.getServerPort());
+            System.out.println(forwarding.getServerPort());
+            forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false);
+            throw new CustomException(ErrorCode.DUPLICATED_SERVER_PORT);
+        }
+
         if (!(dto.getInstanceIp() == null && dto.getInstancePort() == null) &&
-                forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted(forwarding.getInstanceIp()
-                , forwarding.getInstancePort()
+                forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted(
+                        dto.getInstanceIp() == null ? forwarding.getInstanceIp() : dto.getInstanceIp()
+                , dto.getInstancePort() == null ? forwarding.getInstancePort() : dto.getInstancePort()
                 , false)) {
+            System.out.println(dto.getInstanceIp());
+            System.out.println(forwarding.getInstanceIp());
+            System.out.println(forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted(
+                    dto.getInstanceIp() == null ? forwarding.getInstanceIp() : dto.getInstanceIp()
+                    , dto.getInstancePort() == null ? forwarding.getInstancePort() : dto.getInstancePort()
+                    , false));
             throw new CustomException(ErrorCode.DUPLICATED_INSTANCE_INFO);
         }
 
-        if (dto.getServerPort() != null && forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) {
-            throw new CustomException(ErrorCode.DUPLICATED_SERVER_PORT);
-        }
-
         /* 파일 수정 */
+        forwarding.edit(dto);
         String content = forwardingTemplate.getPortForwardingWithTCP(forwarding.getServerPort(),
                 forwarding.getInstanceIp(),
                 forwarding.getInstancePort(),
-- 
GitLab


From 85d12a5613f1c1402a1d714c9b251d74b1163658 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Mon, 17 Mar 2025 17:37:46 +0900
Subject: [PATCH 18/41] =?UTF-8?q?feat:=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20CRU?=
 =?UTF-8?q?D=20=EC=9E=91=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/aolda/itda/config/WebConfig.java |   2 +-
 .../forwarding/ForwardingController.java      |   1 +
 .../controller/routing/RoutingController.java |  39 +++
 .../aolda/itda/dto/routing/RoutingDTO.java    |  37 ++
 .../itda/entity/certificate/Certificate.java  |   4 +-
 .../java/com/aolda/itda/entity/log/Log.java   |   2 +-
 .../aolda/itda/entity/routing/Routing.java    |  34 +-
 .../com/aolda/itda/exception/ErrorCode.java   |  11 +-
 .../certificate/CertificateRepository.java    |   7 +
 .../repository/routing/RoutingRepository.java |  14 +
 .../itda/service/routing/RoutingService.java  | 325 ++++++++++++++++++
 .../aolda/itda/template/OptionTemplate.java   |  12 +-
 .../aolda/itda/template/RoutingTemplate.java  |  44 +++
 13 files changed, 521 insertions(+), 11 deletions(-)
 create mode 100644 src/main/java/com/aolda/itda/controller/routing/RoutingController.java
 create mode 100644 src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java
 create mode 100644 src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java
 create mode 100644 src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java
 create mode 100644 src/main/java/com/aolda/itda/service/routing/RoutingService.java
 create mode 100644 src/main/java/com/aolda/itda/template/RoutingTemplate.java

diff --git a/src/main/java/com/aolda/itda/config/WebConfig.java b/src/main/java/com/aolda/itda/config/WebConfig.java
index fd00807..71e134b 100644
--- a/src/main/java/com/aolda/itda/config/WebConfig.java
+++ b/src/main/java/com/aolda/itda/config/WebConfig.java
@@ -25,7 +25,7 @@ public class WebConfig implements WebMvcConfigurer {
 
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
-        String[] excludeAuth = {"/error", "/api/auth/*" };
+        String[] excludeAuth = {"/error", "/api/auth/*", "/api/*" };
 
         registry.addInterceptor(authInterceptor)
                 .addPathPatterns("/**")
diff --git a/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java
index 19786bd..a6b67ec 100644
--- a/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java
+++ b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java
@@ -2,6 +2,7 @@ package com.aolda.itda.controller.forwarding;
 
 import com.aolda.itda.dto.forwarding.ForwardingDTO;
 import com.aolda.itda.service.forwarding.ForwardingService;
+import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
diff --git a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
new file mode 100644
index 0000000..8943571
--- /dev/null
+++ b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
@@ -0,0 +1,39 @@
+package com.aolda.itda.controller.routing;
+
+import com.aolda.itda.dto.routing.RoutingDTO;
+import com.aolda.itda.service.routing.RoutingService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api")
+@RequiredArgsConstructor
+public class RoutingController {
+
+    private final RoutingService routingService;
+
+    @GetMapping("/routing")
+    public ResponseEntity<Object> view(@RequestParam Long routingId) {
+        return ResponseEntity.ok(routingService.getRouting(routingId));
+    }
+
+    @GetMapping("/routings")
+    public ResponseEntity<Object> lists(@RequestParam String projectId) {
+        return ResponseEntity.ok(routingService.getRoutings(projectId));
+    }
+
+    @PatchMapping("/routing")
+    public ResponseEntity<Object> edit(@RequestParam Long routingId,
+                                       @RequestBody RoutingDTO dto) {
+        routingService.editRouting(routingId, dto);
+        return ResponseEntity.ok().build();
+    }
+
+    @DeleteMapping("/routing")
+    public ResponseEntity<Object> delete(@RequestParam Long routingId) {
+        routingService.deleteRouting(routingId);
+        return ResponseEntity.ok().build();
+    }
+
+}
diff --git a/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java b/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java
new file mode 100644
index 0000000..e95ab70
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java
@@ -0,0 +1,37 @@
+package com.aolda.itda.dto.routing;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class RoutingDTO {
+    @NotBlank
+    private String ip;
+    @NotBlank
+    private String domain;
+    @NotBlank
+    private String name;
+    @NotBlank
+    private String port;
+    private Long id;
+    @NotBlank
+    private Long certificateId;
+
+    private LocalDateTime createdAt;
+
+    private LocalDateTime updatedAt;
+
+    @NotNull
+    private Boolean caching;
+}
diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
index 5ab56a9..188f05e 100644
--- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
+++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
@@ -42,5 +42,7 @@ public class Certificate extends BaseTimeEntity {
 
     private String description;
 
-
+    public String formatDomain() {
+        return domain == null ? null : domain.replace("*", "_");
+    }
 }
diff --git a/src/main/java/com/aolda/itda/entity/log/Log.java b/src/main/java/com/aolda/itda/entity/log/Log.java
index fd8f460..fa78a66 100644
--- a/src/main/java/com/aolda/itda/entity/log/Log.java
+++ b/src/main/java/com/aolda/itda/entity/log/Log.java
@@ -35,6 +35,6 @@ public class Log extends BaseTimeEntity {
     @Enumerated(EnumType.STRING)
     private Action action;
 
-    private String metadata;
+    private String description;
 
 }
diff --git a/src/main/java/com/aolda/itda/entity/routing/Routing.java b/src/main/java/com/aolda/itda/entity/routing/Routing.java
index 0c6b2aa..be03dcd 100644
--- a/src/main/java/com/aolda/itda/entity/routing/Routing.java
+++ b/src/main/java/com/aolda/itda/entity/routing/Routing.java
@@ -1,5 +1,7 @@
 package com.aolda.itda.entity.routing;
 
+import com.aolda.itda.dto.forwarding.ForwardingDTO;
+import com.aolda.itda.dto.routing.RoutingDTO;
 import com.aolda.itda.entity.BaseTimeEntity;
 import com.aolda.itda.entity.certificate.Certificate;
 import com.aolda.itda.entity.user.User;
@@ -36,8 +38,38 @@ public class Routing extends BaseTimeEntity {
 
     private String instanceIp;
 
+    private String instancePort;
+
     private Boolean isDeleted;
 
-    private String description;
+    private Boolean caching;
+
+    private String name;
+
+    public RoutingDTO toRoutingDTO() {
+        return RoutingDTO.builder()
+                .id(routingId)
+                .name(name)
+                .port(instancePort)
+                .ip(instanceIp)
+                .certificateId(certificate == null ? null : certificate.getCertificateId())
+                .caching(caching)
+                .domain(domain)
+                .createdAt(getCreatedAt())
+                .updatedAt(getUpdatedAt())
+                .build();
+    }
+
+    public void edit(RoutingDTO dto, Certificate certificate) {
+        this.name = dto.getName() != null ? dto.getName() : this.name;
+        this.instanceIp = dto.getIp() != null ? dto.getIp() : this.instanceIp;
+        this.instancePort = dto.getPort() != null ? dto.getPort() : this.instancePort;
+        this.caching = dto.getCaching() != null ? dto.getCaching() : this.caching;
+        this.domain = dto.getDomain() != null ? dto.getDomain() : this.domain;
+        this.certificate = certificate;
+    }
 
+    public void delete() {
+        this.isDeleted = true;
+    }
 }
diff --git a/src/main/java/com/aolda/itda/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java
index 443f3a1..321bdad 100644
--- a/src/main/java/com/aolda/itda/exception/ErrorCode.java
+++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java
@@ -15,12 +15,21 @@ public enum ErrorCode {
     INVALID_TOKEN(HttpStatus.BAD_REQUEST, "잘못된 토큰입니다"),
 
     // Forwarding
+    NOT_FOUND_FORWARDING(HttpStatus.BAD_REQUEST, "포트포워딩 파일이 존재하지 않습니다"),
+
+    // Routing
+    NOT_FOUND_ROUTING(HttpStatus.BAD_REQUEST, "라우팅 파일이 존재하지 않습니다"),
+
+    // Certificate
+    NOT_FOUND_CERTIFICATE(HttpStatus.BAD_REQUEST, "SSL 인증서가 존재하지 않습니다"),
+
+    // CONF File
     FAIL_CREATE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 생성하지 못했습니다"),
     FAIL_UPDATE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 수정하지 못했습니다"),
-    NOT_FOUND_FORWARDING(HttpStatus.BAD_REQUEST, "포트포워딩 파일이 존재하지 않습니다"),
     INVALID_CONF_INPUT(HttpStatus.BAD_REQUEST, "잘못된 입력이 존재합니다"),
     DUPLICATED_INSTANCE_INFO(HttpStatus.BAD_REQUEST, "중복된 인스턴스 IP와 포트입니다"),
     DUPLICATED_SERVER_PORT(HttpStatus.BAD_REQUEST, "중복된 서버 포트입니다"),
+    DUPLICATED_DOMAIN_NAME(HttpStatus.BAD_REQUEST, "중복된 도메인 주소입니다"),
 
     // Nginx
     FAIL_NGINX_CONF_TEST(HttpStatus.BAD_REQUEST, "Conf 파일 테스트에 실패했습니다"),
diff --git a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java
new file mode 100644
index 0000000..392c90b
--- /dev/null
+++ b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java
@@ -0,0 +1,7 @@
+package com.aolda.itda.repository.certificate;
+
+import com.aolda.itda.entity.certificate.Certificate;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface CertificateRepository extends JpaRepository<Certificate, Long> {
+}
diff --git a/src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java b/src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java
new file mode 100644
index 0000000..046296c
--- /dev/null
+++ b/src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java
@@ -0,0 +1,14 @@
+package com.aolda.itda.repository.routing;
+
+import com.aolda.itda.entity.forwarding.Forwarding;
+import com.aolda.itda.entity.routing.Routing;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface RoutingRepository extends JpaRepository<Routing, Long> {
+    List<Routing> findByProjectIdAndIsDeleted(String projectId, Boolean isDeleted);
+    Optional<Routing> findByRoutingIdAndIsDeleted(Long routingId, Boolean isDeleted);
+    Boolean existsByDomainAndIsDeleted(String domain, Boolean isDeleted);
+}
diff --git a/src/main/java/com/aolda/itda/service/routing/RoutingService.java b/src/main/java/com/aolda/itda/service/routing/RoutingService.java
new file mode 100644
index 0000000..9cfad7d
--- /dev/null
+++ b/src/main/java/com/aolda/itda/service/routing/RoutingService.java
@@ -0,0 +1,325 @@
+package com.aolda.itda.service.routing;
+
+import com.aolda.itda.dto.PageResp;
+import com.aolda.itda.dto.routing.RoutingDTO;
+import com.aolda.itda.entity.certificate.Certificate;
+import com.aolda.itda.entity.routing.Routing;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.repository.certificate.CertificateRepository;
+import com.aolda.itda.repository.routing.RoutingRepository;
+import com.aolda.itda.template.RoutingTemplate;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.HttpServerErrorException;
+import org.springframework.web.client.RestTemplate;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+@Slf4j
+public class RoutingService {
+
+    private final RoutingRepository routingRepository;
+    private final CertificateRepository certificateRepository;
+    private final RoutingTemplate routingTemplate;
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    /* Routing 조회 */
+    public RoutingDTO getRouting(Long routingId) {
+        // project id 확인 필요
+        Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING));
+        return routing.toRoutingDTO();
+    }
+
+    /* Routing 목록 조회 */
+    public PageResp<RoutingDTO> getRoutings(String projectId) {
+        // project id 확인 필요
+        return PageResp.<RoutingDTO>builder()
+                .contents(routingRepository.findByProjectIdAndIsDeleted(projectId, false)
+                        .stream()
+                        .map(Routing::toRoutingDTO)
+                        .toList()).build();
+    }
+
+    /* Routing 생성 */
+    public void createRouting(String projectId, RoutingDTO dto) {
+        /* 입력 DTO 검증 */
+        validateDTO(dto);
+
+        /* 중복 검증 */
+        if (routingRepository.existsByDomainAndIsDeleted(dto.getDomain(), false)) {
+            throw new CustomException(ErrorCode.DUPLICATED_DOMAIN_NAME);
+        }
+
+        /* SSL 인증서 조회 */
+        Certificate certificate = dto.getCertificateId() == -1 ? null :
+                certificateRepository.findById(dto.getCertificateId())
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_CERTIFICATE)); // isDeleted 확인 필요
+
+        /* 라우팅 엔티티 생성 */
+        Routing routing = Routing.builder()
+                .isDeleted(false)
+                .projectId(projectId)
+                .name(dto.getName())
+                .instanceIp(dto.getIp())
+                .instancePort(dto.getPort())
+                .domain(dto.getDomain())
+                .certificate(certificate)
+                .caching(dto.getCaching())
+                .build();
+
+        routingRepository.save(routing);
+
+        /* nginx conf 파일 생성 및 예외 처리 */
+        String content = routingTemplate.getRouting(dto, certificate == null ? null : certificate.formatDomain());
+        String confPath = "/data/nginx/proxy_host/" + routing.getRoutingId() + ".conf";
+
+        File file = new File(confPath);
+        try {
+            Path path = Paths.get(confPath);
+            Files.createDirectories(path.getParent());
+            if (!file.createNewFile()) {
+                throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "중복된 라우팅 Conf 파일이 존재합니다");
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new CustomException(ErrorCode.FAIL_CREATE_CONF);
+        }
+
+        /* conf 파일 작성 및 예외 처리 */
+        try {
+            BufferedWriter bw = new BufferedWriter(new FileWriter(file, false)); // 예외처리 필요
+            bw.write(content);
+            bw.flush();
+            bw.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+            if (file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_DELETE_CONF);
+            }
+            throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "포트포워딩 Conf 파일을 작성하지 못했습니다");
+        }
+
+        /* nginx test */
+        String url = "http://nginx:8081/nginx-api/test";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (HttpServerErrorException.InternalServerError e) {
+            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
+            if (file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        } catch (Exception e) {
+            log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage());
+            if (file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        }
+
+        /* nginx reload */
+        url = "http://nginx:8081/nginx-api/reload";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (HttpServerErrorException.InternalServerError e) {
+            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
+            if (file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
+        } catch (Exception e) {
+            log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage());
+            if (file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
+        }
+
+    }
+
+    /* Routing 수정 */
+    public void editRouting(Long routingId, RoutingDTO dto) {
+        Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING));
+
+        /* 입력 DTO 검증 */
+        validateDTO(dto);
+
+        /* 중복 검증 */
+        if (dto.getDomain() != null && routingRepository.existsByDomainAndIsDeleted(dto.getDomain(), false)) {
+            throw new CustomException(ErrorCode.DUPLICATED_DOMAIN_NAME);
+        }
+
+        /* SSL 인증서 조회 */
+        Certificate certificate = (dto.getCertificateId() == null) || (dto.getCertificateId() == -1 ) ? null :
+                certificateRepository.findById(dto.getCertificateId())
+                        .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_CERTIFICATE)); // isDeleted 확인 필요
+
+        /* 파일 수정 */
+        routing.edit(dto, certificate);
+        String content = routingTemplate.getRouting(routing.toRoutingDTO(), certificate == null ? null : certificate.formatDomain());
+        String confPath = "/data/nginx/proxy_host/" + routing.getRoutingId() + ".conf";
+        File file = new File(confPath);
+        if (!file.exists()) {
+            throw new CustomException(ErrorCode.NOT_FOUND_FORWARDING, "Conf 파일이 존재하지 않아 수정할 수 없습니다");
+        }
+
+        Path backup;
+        try {
+            backup = Files.createTempFile("temp_", ".tmp");
+            Files.copy(Paths.get(confPath), backup, StandardCopyOption.REPLACE_EXISTING
+                    , StandardCopyOption.COPY_ATTRIBUTES);
+
+            BufferedWriter bw = new BufferedWriter(new FileWriter(file, false));
+            bw.write(content);
+            bw.flush();
+            bw.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "라우팅 Conf 파일을 수정하지 못했습니다");
+        }
+
+        /* nginx test */
+        String url = "http://nginx:8081/nginx-api/test";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (HttpServerErrorException.InternalServerError e) {
+            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
+            try {
+                Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
+                        , StandardCopyOption.COPY_ATTRIBUTES);
+                Files.delete(backup);
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(라우팅 Conf 파일 수정)");
+            }
+
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        } catch (RuntimeException e) {
+            log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage());
+            try {
+                Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
+                        , StandardCopyOption.COPY_ATTRIBUTES);
+                Files.delete(backup);
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(라우팅 Conf 파일 수정)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        }
+
+        /* nginx reload */
+        url = "http://nginx:8081/nginx-api/reload";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (HttpServerErrorException.InternalServerError e) {
+            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
+            try {
+                Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
+                        , StandardCopyOption.COPY_ATTRIBUTES);
+                Files.delete(backup);
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(라우팅 Conf 파일 수정)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
+        } catch (RuntimeException e) {
+            log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage());
+            try {
+                Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
+                        , StandardCopyOption.COPY_ATTRIBUTES);
+                Files.delete(backup);
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(라우팅 Conf 파일 수정)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
+        }
+
+        /* DB 정보 수정 */
+        routingRepository.save(routing);
+    }
+
+    /* Routing 삭제 */
+    public void deleteRouting(Long routingId) {
+        Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING));
+
+        /* 파일 삭제 */
+        String confPath = "/data/nginx/proxy_host/" + routing.getRoutingId() + ".conf";
+        String deletePath = confPath + ".deleted";
+        try {
+            Files.move(Paths.get(confPath), Paths.get(deletePath));
+        } catch (IOException e) {
+            throw new CustomException(ErrorCode.FAIL_DELETE_CONF);
+        }
+
+        /* nginx test */
+        String url = "http://nginx:8081/nginx-api/test";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (HttpServerErrorException.InternalServerError e) {
+            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
+            try {
+                Files.move(Paths.get(deletePath), Paths.get(confPath));
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(라우팅 Conf 삭제)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        } catch (Exception e) {
+            log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage());
+            try {
+                Files.move(Paths.get(deletePath), Paths.get(confPath));
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(라우팅 Conf 삭제)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        }
+
+        /* nginx reload */
+        url = "http://nginx:8081/nginx-api/reload";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (HttpServerErrorException.InternalServerError e) {
+            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
+            try {
+                Files.move(Paths.get(deletePath), Paths.get(confPath));
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(라우팅 Conf 삭제)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
+        } catch (Exception e) {
+            log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage());
+            try {
+                Files.move(Paths.get(deletePath), Paths.get(confPath));
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(라우팅 Conf 삭제)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
+        }
+
+        /* DB */
+        routing.delete();
+        routingRepository.save(routing);
+    }
+
+    private void validateDTO(RoutingDTO dto) {
+
+        for (ConstraintViolation<RoutingDTO> violation : Validation.buildDefaultValidatorFactory().getValidator().validate(dto)) {
+            throw new CustomException(ErrorCode.INVALID_CONF_INPUT, violation.getMessage());
+        }
+
+    }
+}
diff --git a/src/main/java/com/aolda/itda/template/OptionTemplate.java b/src/main/java/com/aolda/itda/template/OptionTemplate.java
index 3e1e92c..945b666 100644
--- a/src/main/java/com/aolda/itda/template/OptionTemplate.java
+++ b/src/main/java/com/aolda/itda/template/OptionTemplate.java
@@ -5,22 +5,22 @@ import org.springframework.stereotype.Component;
 @Component
 public class OptionTemplate {
 
-    public String getSSL(Long certificateId) {
+    public String getSSL(String certificateDomain) {
         return "\nconf.d/include/letsencrypt-acme-challenge.conf;\n" +
                 "include conf.d/include/ssl-ciphers.conf;\n" +
-                "ssl_certificate /etc/letsencrypt/live/npm-" + certificateId + "/fullchain.pem;\n" +
-                "ssl_certificate_key /etc/letsencrypt/live/npm-" + certificateId + "/privkey.pem;\n";
+                "ssl_certificate /data/lego/certificates/" + certificateDomain + ".crt;\n" +
+                "ssl_certificate_key /data/lego/certificates/" + certificateDomain + ".key;\n";
     }
 
     public String getAssetCaching() {
-        return "include conf.d/include/assets.conf;\n";
+        return "\ninclude conf.d/include/assets.conf;\n";
     }
 
     public String getBlockExploits() {
-        return "include conf.d/include/block-exploits.conf;\n";
+        return "\ninclude conf.d/include/block-exploits.conf;\n";
     }
 
     public String getForceSSL() {
-        return "include conf.d/include/force-ssl.conf;\n";
+        return "\ninclude conf.d/include/force-ssl.conf;\n";
     }
 }
diff --git a/src/main/java/com/aolda/itda/template/RoutingTemplate.java b/src/main/java/com/aolda/itda/template/RoutingTemplate.java
new file mode 100644
index 0000000..46e6116
--- /dev/null
+++ b/src/main/java/com/aolda/itda/template/RoutingTemplate.java
@@ -0,0 +1,44 @@
+package com.aolda.itda.template;
+
+import com.aolda.itda.dto.routing.RoutingDTO;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class RoutingTemplate {
+
+    private final OptionTemplate optionTemplate;
+
+    public String getRouting(RoutingDTO dto, String certificateDomain) {
+        return "server { \n"
+                + "set $forward_scheme http;\n"
+                + "set $server \"" + dto.getIp() +"\";\n"
+                + "set $port "+ dto.getPort() +";\n"
+                + "\n"
+                + "listen 80;\n"
+                + "listen [::]:80;\n"
+                + (dto.getCertificateId() == -1 ? "" :
+                "listen 443 ssl;\n listen [::]:443 ssl;\n")
+                + "server_name " + dto.getDomain() + ";\n"
+                + (dto.getCertificateId() == -1 ? "" :
+                optionTemplate.getSSL(certificateDomain))
+                + (dto.getCaching() ? "" :
+                optionTemplate.getAssetCaching()
+        )
+                + "proxy_set_header Upgrade $http_upgrade;\n"
+                + "proxy_set_header Connection $http_connection;\n"
+                + "proxy_http_version 1.1;\n"
+                + "\n"
+                + "access_log /data/logs/proxy-host-" + dto.getId() + "_access.log proxy;\n"
+                + "error_log /data/logs/proxy-host-" + dto.getId() + "_error.log warn;\n"
+                + "location / { \n"
+                + "proxy_set_header Upgrade $http_upgrade;\n"
+                + "proxy_set_header Connection $http_connection;\n"
+                + "proxy_http_version 1.1;\n"
+                + "include conf.d/include/proxy.conf;\n"
+                + "}\n"
+                + "}\n";
+    }
+
+}
-- 
GitLab


From 783724adc2cf3f660a53565a576df6a4865b3b88 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Mon, 17 Mar 2025 21:43:19 +0900
Subject: [PATCH 19/41] =?UTF-8?q?fix:=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20Crea?=
 =?UTF-8?q?te=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EB=B9=A0=EC=A0=B8?=
 =?UTF-8?q?=EC=9E=88=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../aolda/itda/controller/routing/RoutingController.java  | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
index 8943571..aec9927 100644
--- a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
+++ b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
@@ -1,5 +1,6 @@
 package com.aolda.itda.controller.routing;
 
+import com.aolda.itda.dto.forwarding.ForwardingDTO;
 import com.aolda.itda.dto.routing.RoutingDTO;
 import com.aolda.itda.service.routing.RoutingService;
 import lombok.RequiredArgsConstructor;
@@ -13,6 +14,13 @@ public class RoutingController {
 
     private final RoutingService routingService;
 
+    @PostMapping("/forwarding")
+    public ResponseEntity<Object> create(@RequestParam String projectId,
+                                         @RequestBody RoutingDTO dto) {
+        routingService.createRouting(projectId, dto);
+        return ResponseEntity.ok().build();
+    }
+
     @GetMapping("/routing")
     public ResponseEntity<Object> view(@RequestParam Long routingId) {
         return ResponseEntity.ok(routingService.getRouting(routingId));
-- 
GitLab


From 585f5bb27620012fa5f77904532a98adf4b96ac7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Tue, 18 Mar 2025 13:42:46 +0900
Subject: [PATCH 20/41] =?UTF-8?q?fix:=20API=20=EA=B2=BD=EB=A1=9C,=20DB=20?=
 =?UTF-8?q?=ED=95=84=EB=93=9C=EA=B0=80=20=EC=9E=98=EB=AA=BB=EB=90=98?=
 =?UTF-8?q?=EC=96=B4=EC=9E=88=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?=
 =?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/aolda/itda/controller/routing/RoutingController.java  | 2 +-
 src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java      | 2 +-
 src/main/java/com/aolda/itda/entity/routing/Routing.java      | 4 ++--
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
index aec9927..dc4d0db 100644
--- a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
+++ b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
@@ -14,7 +14,7 @@ public class RoutingController {
 
     private final RoutingService routingService;
 
-    @PostMapping("/forwarding")
+    @PostMapping("/routing")
     public ResponseEntity<Object> create(@RequestParam String projectId,
                                          @RequestBody RoutingDTO dto) {
         routingService.createRouting(projectId, dto);
diff --git a/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java b/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java
index e95ab70..6bc48d8 100644
--- a/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java
+++ b/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java
@@ -25,7 +25,7 @@ public class RoutingDTO {
     @NotBlank
     private String port;
     private Long id;
-    @NotBlank
+    @NotNull
     private Long certificateId;
 
     private LocalDateTime createdAt;
diff --git a/src/main/java/com/aolda/itda/entity/routing/Routing.java b/src/main/java/com/aolda/itda/entity/routing/Routing.java
index be03dcd..39b7716 100644
--- a/src/main/java/com/aolda/itda/entity/routing/Routing.java
+++ b/src/main/java/com/aolda/itda/entity/routing/Routing.java
@@ -25,11 +25,11 @@ public class Routing extends BaseTimeEntity {
     private Long routingId;
 
     @OneToOne
-    @JoinColumn(name = "user_id", nullable = false)
+    @JoinColumn(name = "user_id")
     private User user;
 
     @OneToOne
-    @JoinColumn(name = "certificate_id", nullable = false)
+    @JoinColumn(name = "certificate_id")
     private Certificate certificate;
 
     private String projectId;
-- 
GitLab


From 3f8e2f278bdcf3fe0e9988cf58e3d74d19741289 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Tue, 18 Mar 2025 16:23:39 +0900
Subject: [PATCH 21/41] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?=
 =?UTF-8?q?=ED=8A=B8=EB=B3=84=20=EA=B6=8C=ED=95=9C=20=EA=B2=80=EC=A6=9D=20?=
 =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../aolda/itda/config/AuthInterceptor.java    | 47 +++++++++++++++----
 .../forwarding/ForwardingController.java      | 17 ++++---
 .../controller/routing/RoutingController.java | 18 ++++---
 .../com/aolda/itda/exception/ErrorCode.java   |  1 +
 .../com/aolda/itda/service/AuthService.java   |  8 +++-
 .../service/forwarding/ForwardingService.java | 20 ++++++--
 .../itda/service/routing/RoutingService.java  | 20 ++++++--
 7 files changed, 103 insertions(+), 28 deletions(-)

diff --git a/src/main/java/com/aolda/itda/config/AuthInterceptor.java b/src/main/java/com/aolda/itda/config/AuthInterceptor.java
index 63f2320..83c3821 100644
--- a/src/main/java/com/aolda/itda/config/AuthInterceptor.java
+++ b/src/main/java/com/aolda/itda/config/AuthInterceptor.java
@@ -1,8 +1,10 @@
 package com.aolda.itda.config;
 
+import com.aolda.itda.dto.auth.ProjectIdAndNameDTO;
 import com.aolda.itda.exception.CustomException;
 import com.aolda.itda.exception.ErrorCode;
 import com.aolda.itda.service.AuthService;
+import com.fasterxml.jackson.core.JsonProcessingException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.RequiredArgsConstructor;
@@ -10,6 +12,10 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 import org.springframework.web.servlet.HandlerInterceptor;
 
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
 @RequiredArgsConstructor
 @Component
 @Slf4j
@@ -20,18 +26,43 @@ public class AuthInterceptor implements HandlerInterceptor {
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
         String token = request.getHeader("X-Subject-Token");
+
+        /* 토큰 헤더 검증 */
         if (token == null || token.isEmpty()) {
             throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI());
         }
-        // 어드민과 일반 유저 구분 필요
-        try {
-            if (authService.validateTokenAndGetUserId(token) != null) {
-                return true;
-            }
-        } catch (Exception e) {
-            log.error("Token validation failed for URI {}: {}", request.getRequestURI(), e.getMessage(), e);
+
+        /* 유효 토큰 검증 */
+        String userId = authService.validateTokenAndGetUserId(token);
+        if (userId == null) {
+            log.error("Token validation failed for URI {}: {}", request.getRequestURI(), request.getRemoteAddr());
             throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI());
         }
-        throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI());
+
+        /* 프로젝트 권한 검증 */
+        String projectId = request.getParameter("projectId");
+        if (projectId != null) {
+
+            try {
+                String role = authService.getBestRoleWithinProject(token, projectId).get("role");
+                if (!role.equals("admin")) {
+                    log.error("Unauthorized Token for URI {}: {}", request.getRequestURI(), request.getRemoteAddr());
+                    throw new CustomException(ErrorCode.UNAUTHORIZED_USER, request.getRequestURI());
+                }
+            } catch (Exception e) {
+                throw new CustomException(ErrorCode.UNAUTHORIZED_USER, request.getRequestURI());
+            }
+
+
+
+        }
+
+        /* 프로젝트 리스트 조회 */
+        List<String> projects = authService.getProjectsWithUser(Map.of("id", userId, "token", token))
+                        .stream().map(ProjectIdAndNameDTO::getId)
+                        .toList();
+        request.setAttribute("projects", projects);
+        return true;
+
     }
 }
diff --git a/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java
index a6b67ec..36e6f9a 100644
--- a/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java
+++ b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java
@@ -2,11 +2,14 @@ package com.aolda.itda.controller.forwarding;
 
 import com.aolda.itda.dto.forwarding.ForwardingDTO;
 import com.aolda.itda.service.forwarding.ForwardingService;
+import jakarta.servlet.http.HttpServletRequest;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.List;
+
 @RestController
 @RequestMapping("/api")
 @RequiredArgsConstructor
@@ -22,8 +25,8 @@ public class ForwardingController {
     }
 
     @GetMapping("/forwarding")
-    public ResponseEntity<Object> view(@RequestParam Long forwardingId) {
-        return ResponseEntity.ok(forwardingService.getForwarding(forwardingId));
+    public ResponseEntity<Object> view(@RequestParam Long forwardingId, HttpServletRequest request) {
+        return ResponseEntity.ok(forwardingService.getForwarding(forwardingId, (List<String>) request.getAttribute("projects")));
     }
 
     @GetMapping("/forwardings")
@@ -33,14 +36,16 @@ public class ForwardingController {
 
     @PatchMapping("/forwarding")
     public ResponseEntity<Object> edit(@RequestParam Long forwardingId,
-                                         @RequestBody ForwardingDTO dto) {
-        forwardingService.editForwarding(forwardingId, dto);
+                                         @RequestBody ForwardingDTO dto,
+                                       HttpServletRequest request) {
+        forwardingService.editForwarding(forwardingId, dto, (List<String>) request.getAttribute("projects") );
         return ResponseEntity.ok().build();
     }
 
     @DeleteMapping("/forwarding")
-    public ResponseEntity<Object> delete(@RequestParam Long forwardingId) {
-        forwardingService.deleteForwarding(forwardingId);
+    public ResponseEntity<Object> delete(@RequestParam Long forwardingId,
+                                         HttpServletRequest request) {
+        forwardingService.deleteForwarding(forwardingId, (List<String>) request.getAttribute("projects"));
         return ResponseEntity.ok().build();
     }
 
diff --git a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
index dc4d0db..4b9d551 100644
--- a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
+++ b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
@@ -3,10 +3,13 @@ package com.aolda.itda.controller.routing;
 import com.aolda.itda.dto.forwarding.ForwardingDTO;
 import com.aolda.itda.dto.routing.RoutingDTO;
 import com.aolda.itda.service.routing.RoutingService;
+import jakarta.servlet.http.HttpServletRequest;
 import lombok.RequiredArgsConstructor;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.List;
+
 @RestController
 @RequestMapping("/api")
 @RequiredArgsConstructor
@@ -22,8 +25,9 @@ public class RoutingController {
     }
 
     @GetMapping("/routing")
-    public ResponseEntity<Object> view(@RequestParam Long routingId) {
-        return ResponseEntity.ok(routingService.getRouting(routingId));
+    public ResponseEntity<Object> view(@RequestParam Long routingId,
+                                       HttpServletRequest request) {
+        return ResponseEntity.ok(routingService.getRouting(routingId, (List<String>) request.getAttribute("projects")));
     }
 
     @GetMapping("/routings")
@@ -33,14 +37,16 @@ public class RoutingController {
 
     @PatchMapping("/routing")
     public ResponseEntity<Object> edit(@RequestParam Long routingId,
-                                       @RequestBody RoutingDTO dto) {
-        routingService.editRouting(routingId, dto);
+                                       @RequestBody RoutingDTO dto,
+                                       HttpServletRequest request) {
+        routingService.editRouting(routingId, dto, (List<String>) request.getAttribute("projects"));
         return ResponseEntity.ok().build();
     }
 
     @DeleteMapping("/routing")
-    public ResponseEntity<Object> delete(@RequestParam Long routingId) {
-        routingService.deleteRouting(routingId);
+    public ResponseEntity<Object> delete(@RequestParam Long routingId,
+                                         HttpServletRequest request) {
+        routingService.deleteRouting(routingId, (List<String>) request.getAttribute("projects"));
         return ResponseEntity.ok().build();
     }
 
diff --git a/src/main/java/com/aolda/itda/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java
index 321bdad..6eea5fb 100644
--- a/src/main/java/com/aolda/itda/exception/ErrorCode.java
+++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java
@@ -10,6 +10,7 @@ public enum ErrorCode {
 
     // User
     INVALID_USER_INFO(HttpStatus.BAD_REQUEST, "잘못된 회원 정보입니다"),
+    UNAUTHORIZED_USER(HttpStatus.BAD_REQUEST, "권한이 없는 사용자입니다"),
 
     // Token
     INVALID_TOKEN(HttpStatus.BAD_REQUEST, "잘못된 토큰입니다"),
diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java
index cc523fa..cbc157f 100644
--- a/src/main/java/com/aolda/itda/service/AuthService.java
+++ b/src/main/java/com/aolda/itda/service/AuthService.java
@@ -239,7 +239,7 @@ public class AuthService {
     }
 
     // 특정 사용자의 참여 프로젝트 반환
-    private List<ProjectIdAndNameDTO> getProjectsWithUser(Map<String, String> user) throws JsonProcessingException {
+    public List<ProjectIdAndNameDTO> getProjectsWithUser(Map<String, String> user) throws JsonProcessingException {
         String userId = user.get("id");
         String token = user.get("token");
         if (userId == null || token == null) {
@@ -283,6 +283,12 @@ public class AuthService {
 
     }
 
+    public void validateProjectAuth(List<String> projects, String projectId) {
+        if (!projects.contains(projectId)) {
+            throw new CustomException(ErrorCode.UNAUTHORIZED_USER);
+        }
+    }
+
     private Boolean isAdmin(Map<String, String> user) throws JsonProcessingException {
         String url = keystone + "/role_assignments?user.id=" + user.get("id") + "&scope.system&include_names";
         HttpHeaders headers = new HttpHeaders();
diff --git a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
index 9ec9343..441090b 100644
--- a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
+++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
@@ -1,13 +1,16 @@
 package com.aolda.itda.service.forwarding;
 
 import com.aolda.itda.dto.PageResp;
+import com.aolda.itda.dto.auth.ProjectIdAndNameDTO;
 import com.aolda.itda.dto.forwarding.ForwardingDTO;
 import com.aolda.itda.entity.forwarding.Forwarding;
 import com.aolda.itda.exception.CustomException;
 import com.aolda.itda.exception.ErrorCode;
 import com.aolda.itda.repository.forwarding.ForwardingRepository;
+import com.aolda.itda.service.AuthService;
 import com.aolda.itda.template.ForwardingTemplate;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.http.HttpServletRequest;
 import jakarta.validation.ConstraintViolation;
 import jakarta.validation.Validation;
 import lombok.RequiredArgsConstructor;
@@ -30,6 +33,7 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
+import java.util.List;
 import java.util.Map;
 
 @Service
@@ -42,12 +46,17 @@ public class ForwardingService {
     private String serverBaseIp;
     private final ForwardingTemplate forwardingTemplate;
     private final ForwardingRepository forwardingRepository;
+    private final AuthService authService;
     private final RestTemplate restTemplate = new RestTemplate();
 
     /* 포트포워딩 정보 조회 */
-    public ForwardingDTO getForwarding(Long forwardingId) {
+    public ForwardingDTO getForwarding(Long forwardingId, List<String> projects) {
         Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false)
                 .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
+
+        /* 프로젝트 권한 검증 */
+        authService.validateProjectAuth(projects, forwarding.getProjectId());
+
         return forwarding.toForwardingDTO();
     }
 
@@ -158,10 +167,12 @@ public class ForwardingService {
     }
 
     /* 포트포워딩 정보 수정 */
-    public void editForwarding(Long forwardingId, ForwardingDTO dto) {
+    public void editForwarding(Long forwardingId, ForwardingDTO dto, List<String> projects) {
         Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false)
                 .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
 
+        /* 프로젝트 권한 검증 */
+        authService.validateProjectAuth(projects, forwarding.getProjectId());
 
         /* 중복 검증 */
         if (dto.getServerPort() != null && forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) {
@@ -270,10 +281,13 @@ public class ForwardingService {
     }
 
     /* 포트포워딩 삭제 (소프트) */
-    public void deleteForwarding(Long forwardingId) {
+    public void deleteForwarding(Long forwardingId, List<String> projects) {
         Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false)
                 .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
 
+        /* 프로젝트 권한 검증 */
+        authService.validateProjectAuth(projects, forwarding.getProjectId());
+
         /* 파일 삭제 */
         String confPath = "/data/nginx/stream/" + forwarding.getForwardingId() + ".conf";
         String deletePath = confPath + ".deleted";
diff --git a/src/main/java/com/aolda/itda/service/routing/RoutingService.java b/src/main/java/com/aolda/itda/service/routing/RoutingService.java
index 9cfad7d..eb3777b 100644
--- a/src/main/java/com/aolda/itda/service/routing/RoutingService.java
+++ b/src/main/java/com/aolda/itda/service/routing/RoutingService.java
@@ -8,6 +8,7 @@ import com.aolda.itda.exception.CustomException;
 import com.aolda.itda.exception.ErrorCode;
 import com.aolda.itda.repository.certificate.CertificateRepository;
 import com.aolda.itda.repository.routing.RoutingRepository;
+import com.aolda.itda.service.AuthService;
 import com.aolda.itda.template.RoutingTemplate;
 import jakarta.validation.ConstraintViolation;
 import jakarta.validation.Validation;
@@ -26,6 +27,7 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
+import java.util.List;
 
 @Service
 @Transactional
@@ -35,14 +37,18 @@ public class RoutingService {
 
     private final RoutingRepository routingRepository;
     private final CertificateRepository certificateRepository;
+    private final AuthService authService;
     private final RoutingTemplate routingTemplate;
     private final RestTemplate restTemplate = new RestTemplate();
 
     /* Routing 조회 */
-    public RoutingDTO getRouting(Long routingId) {
-        // project id 확인 필요
+    public RoutingDTO getRouting(Long routingId, List<String> projects) {
         Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false)
                 .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING));
+
+        /* 프로젝트 권한 검증 */
+        authService.validateProjectAuth(projects, routing.getProjectId());
+
         return routing.toRoutingDTO();
     }
 
@@ -154,10 +160,13 @@ public class RoutingService {
     }
 
     /* Routing 수정 */
-    public void editRouting(Long routingId, RoutingDTO dto) {
+    public void editRouting(Long routingId, RoutingDTO dto, List<String> projects) {
         Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false)
                 .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING));
 
+        /* 프로젝트 권한 검증 */
+        authService.validateProjectAuth(projects, routing.getProjectId());
+
         /* 입력 DTO 검증 */
         validateDTO(dto);
 
@@ -253,10 +262,13 @@ public class RoutingService {
     }
 
     /* Routing 삭제 */
-    public void deleteRouting(Long routingId) {
+    public void deleteRouting(Long routingId, List<String> projects) {
         Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false)
                 .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING));
 
+        /* 프로젝트 권한 검증 */
+        authService.validateProjectAuth(projects, routing.getProjectId());
+
         /* 파일 삭제 */
         String confPath = "/data/nginx/proxy_host/" + routing.getRoutingId() + ".conf";
         String deletePath = confPath + ".deleted";
-- 
GitLab


From ad7d22e661ac3b0f41f779c4f6ae023466791501 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Sat, 22 Mar 2025 22:28:17 +0900
Subject: [PATCH 22/41] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20CUD?=
 =?UTF-8?q?=20=ED=99=9C=EB=8F=99=20=EB=A1=9C=EA=B7=B8=20=EC=83=9D=EC=84=B1?=
 =?UTF-8?q?=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 build.gradle                                  |   9 +-
 .../itda/aspect/ForwardingLogAspect.java      | 154 +++++++++++++++++
 .../aolda/itda/aspect/RoutingLogAspect.java   | 157 ++++++++++++++++++
 .../aolda/itda/config/AuthInterceptor.java    |   7 +-
 .../java/com/aolda/itda/config/WebConfig.java |  10 +-
 .../itda/controller/log/LogController.java    |  41 +++++
 .../java/com/aolda/itda/dto/PageResp.java     |   2 +-
 .../com/aolda/itda/dto/auth/IdAndNameDTO.java |  24 +++
 .../aolda/itda/dto/auth/LoginResponseDTO.java |   2 +-
 .../itda/dto/auth/ProjectIdAndNameDTO.java    |  16 --
 .../java/com/aolda/itda/dto/log/LogDTO.java   |  38 +++++
 .../com/aolda/itda/entity/BaseTimeEntity.java |   3 +
 .../itda/entity/forwarding/Forwarding.java    |  18 +-
 .../java/com/aolda/itda/entity/log/Log.java   |  14 ++
 .../com/aolda/itda/exception/ErrorCode.java   |   4 +
 .../itda/repository/log/LogQueryDSL.java      | 104 ++++++++++++
 .../itda/repository/log/LogRepository.java    |  10 ++
 .../itda/repository/user/UserRepository.java  |  11 ++
 .../com/aolda/itda/service/AuthService.java   |  22 ++-
 .../service/forwarding/ForwardingService.java |  12 +-
 .../aolda/itda/service/log/LogService.java    |  57 +++++++
 .../itda/service/routing/RoutingService.java  |  11 +-
 22 files changed, 676 insertions(+), 50 deletions(-)
 create mode 100644 src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java
 create mode 100644 src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java
 create mode 100644 src/main/java/com/aolda/itda/controller/log/LogController.java
 create mode 100644 src/main/java/com/aolda/itda/dto/auth/IdAndNameDTO.java
 delete mode 100644 src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java
 create mode 100644 src/main/java/com/aolda/itda/dto/log/LogDTO.java
 create mode 100644 src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java
 create mode 100644 src/main/java/com/aolda/itda/repository/log/LogRepository.java
 create mode 100644 src/main/java/com/aolda/itda/repository/user/UserRepository.java
 create mode 100644 src/main/java/com/aolda/itda/service/log/LogService.java

diff --git a/build.gradle b/build.gradle
index b88f454..1b29e4a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -30,12 +30,19 @@ dependencies {
     compileOnly 'org.projectlombok:lombok'
     runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
     runtimeOnly 'com.mysql:mysql-connector-j'
-    implementation 'org.pacesys:openstack4j:3.1.0'
     annotationProcessor 'org.projectlombok:lombok'
     testImplementation 'org.springframework.boot:spring-boot-starter-test'
     testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
+    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
+    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
+    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
+
 }
 
 tasks.named('test') {
     useJUnitPlatform()
 }
+clean {
+    delete file('src/main/generated')
+}
\ No newline at end of file
diff --git a/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java b/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java
new file mode 100644
index 0000000..f515ab6
--- /dev/null
+++ b/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java
@@ -0,0 +1,154 @@
+package com.aolda.itda.aspect;
+
+import com.aolda.itda.dto.forwarding.ForwardingDTO;
+import com.aolda.itda.entity.forwarding.Forwarding;
+import com.aolda.itda.entity.log.Action;
+import com.aolda.itda.entity.log.Log;
+import com.aolda.itda.entity.log.ObjectType;
+import com.aolda.itda.entity.user.User;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.repository.certificate.CertificateRepository;
+import com.aolda.itda.repository.forwarding.ForwardingRepository;
+import com.aolda.itda.repository.log.LogRepository;
+import com.aolda.itda.repository.routing.RoutingRepository;
+import com.aolda.itda.repository.user.UserRepository;
+import com.aolda.itda.service.AuthService;
+import jakarta.persistence.EntityManager;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.util.Map;
+import java.util.Objects;
+
+@Aspect
+@Component
+@RequiredArgsConstructor
+public class ForwardingLogAspect {
+
+    private final ForwardingRepository forwardingRepository;
+    private final LogRepository logRepository;
+    private final EntityManager entityManager;
+    private final UserRepository userRepository;
+
+    /* Create 로깅 */
+    @AfterReturning(pointcut = "execution(* com.aolda.itda.service.forwarding.*Service.*create*(..))"
+    , returning = "result")
+    public void createLogging(JoinPoint joinPoint, ForwardingDTO result) {
+
+        /* 사용자 조회 */
+        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+        Map<String, String> tmp = (Map<String, String>) request.getSession().getAttribute("user");
+        User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
+                () -> new CustomException(ErrorCode.NOT_FOUND_USER)
+        );
+
+        /* 생성된 엔티티 조회 */
+        Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(result.getId(), false).orElse(null);
+
+        /* 로그 메세지 작성 */
+        String description = "name: " + forwarding.getName() + "\n"
+                + "serverPort: " + forwarding.getServerPort() + "\n"
+                + "instanceIp: " + forwarding.getInstanceIp() + "\n"
+                + "instancePort: " + forwarding.getInstancePort();
+
+        /* 로그 엔티티 저장 */
+        logRepository.save(Log.builder()
+                .user(user)
+                .objectType(ObjectType.FORWARDING)
+                .objectId(forwarding.getForwardingId())
+                .action(Action.UPDATE)
+                .projectId(forwarding.getProjectId())
+                .description(description)
+                .build());
+    }
+
+    /* Delete 로깅 */
+    @AfterReturning(pointcut = "execution(* com.aolda.itda.service.forwarding.*Service.*delete*(..))")
+    public void deleteLogging(JoinPoint joinPoint) {
+        
+        /* 사용자 조회 */
+        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+        Map<String, String> tmp = (Map<String, String>) request.getSession().getAttribute("user");
+        User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
+                () -> new CustomException(ErrorCode.NOT_FOUND_USER)
+        );
+
+        /* 삭제된 엔티티 조회 */
+        Object[] args = joinPoint.getArgs();
+
+        Long id = (Long) args[0];
+        Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(id, true).orElse(null);
+
+        /* 로그 메세지 작성 */
+        String description = "name: " + forwarding.getName() + "\n"
+                + "serverPort: " + forwarding.getServerPort() + "\n"
+                + "instanceIp: " + forwarding.getInstanceIp() + "\n"
+                + "instancePort: " + forwarding.getInstancePort();
+
+        /* 로그 엔티티 저장 */
+        logRepository.save(Log.builder()
+                .user(user)
+                .objectType(ObjectType.FORWARDING)
+                .objectId(forwarding.getForwardingId())
+                .action(Action.UPDATE)
+                .projectId(forwarding.getProjectId())
+                .description(description)
+                .build());
+    }
+
+    /* Update(edit) 로깅 */
+    @Around("execution(* com.aolda.itda.service.forwarding.*Service.*edit*(..))")
+    public Object editLogging(ProceedingJoinPoint joinPoint) throws Throwable {
+        
+        /* 사용자 조회 */
+        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+        Map<String, String> tmp = (Map<String, String>) request.getSession().getAttribute("user");
+        User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
+                () -> new CustomException(ErrorCode.NOT_FOUND_USER)
+        );
+
+        /* 변경 전 엔티티 조회 */
+        Object[] args = joinPoint.getArgs();
+
+        Long id = (Long) args[0];
+        Forwarding old = forwardingRepository.findByForwardingIdAndIsDeleted(id, false).orElse(null);
+        if (old != null) {
+            entityManager.detach(old);
+        }
+        
+        /* 메소드 진행 */
+        Object result = joinPoint.proceed();
+
+        /* 변경 후 엔티티 조회*/
+        Forwarding newObj = forwardingRepository.findByForwardingIdAndIsDeleted(id, false).orElse(null);
+        
+        /* 로그 메세지 작성 */
+        String description = "name: " + old.getName() + (old.getName().equals(newObj.getName()) ? "" : (" -> " + newObj.getName())) + "\n"
+                + "serverPort: " + old.getServerPort() + (old.getServerPort().equals(newObj.getServerPort()) ? "" : (" -> " + newObj.getServerPort())) + "\n"
+                + "instanceIp: " + (old.getInstanceIp().equals(newObj.getInstanceIp()) ? "" : (" -> " + newObj.getInstanceIp())) + "\n"
+                + "instancePort: " + (old.getInstancePort().equals(newObj.getInstancePort()) ? "" : (" -> " + newObj.getInstancePort()));
+
+        /* 로그 엔티티 저장 */
+        logRepository.save(Log.builder()
+                .user(user)
+                .objectType(ObjectType.FORWARDING)
+                .objectId(newObj.getForwardingId())
+                .action(Action.UPDATE)
+                .projectId(newObj.getProjectId())
+                .description(description)
+                .build());
+        return result;
+    }
+
+
+
+}
diff --git a/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java
new file mode 100644
index 0000000..c8b052a
--- /dev/null
+++ b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java
@@ -0,0 +1,157 @@
+package com.aolda.itda.aspect;
+
+import com.aolda.itda.dto.forwarding.ForwardingDTO;
+import com.aolda.itda.dto.routing.RoutingDTO;
+import com.aolda.itda.entity.forwarding.Forwarding;
+import com.aolda.itda.entity.log.Action;
+import com.aolda.itda.entity.log.Log;
+import com.aolda.itda.entity.log.ObjectType;
+import com.aolda.itda.entity.routing.Routing;
+import com.aolda.itda.entity.user.User;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.repository.forwarding.ForwardingRepository;
+import com.aolda.itda.repository.log.LogRepository;
+import com.aolda.itda.repository.routing.RoutingRepository;
+import com.aolda.itda.repository.user.UserRepository;
+import jakarta.persistence.EntityManager;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.util.Map;
+import java.util.Objects;
+
+@Aspect
+@Component
+@RequiredArgsConstructor
+public class RoutingLogAspect {
+
+    private final RoutingRepository routingRepository;
+    private final UserRepository userRepository;
+    private final LogRepository logRepository;
+    private final EntityManager entityManager;
+
+    /* Create 로깅 */
+    @AfterReturning(pointcut = "execution(* com.aolda.itda.service.routing.*Service.*create*(..))"
+            , returning = "result")
+    public void createLogging(JoinPoint joinPoint, RoutingDTO result) {
+
+        /* 사용자 조회 */
+        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+        Map<String, String> tmp = (Map<String, String>) request.getSession().getAttribute("user");
+        User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
+                () -> new CustomException(ErrorCode.NOT_FOUND_USER)
+        );
+
+        /* 생성된 엔티티 조회 */
+        Routing routing = routingRepository.findByRoutingIdAndIsDeleted(result.getId(), false).orElse(null);
+
+        /* 로그 메세지 작성 */
+        String description = "name: " + routing.getName() + "\n"
+                + "domain: " + routing.getDomain() + "\n"
+                + "ip: " + routing.getInstanceIp() + "\n"
+                + "ip: " + routing.getInstancePort() + "\n"
+                + "certificateId: " + routing.getCertificate().getCertificateId() + "\n"
+                + "caching: " + routing.getCaching() + "\n";
+
+        /* 로그 엔티티 저장 */
+        logRepository.save(Log.builder()
+                .user(user)
+                .objectType(ObjectType.FORWARDING)
+                .objectId(routing.getRoutingId())
+                .action(Action.UPDATE)
+                .projectId(routing.getProjectId())
+                .description(description)
+                .build());
+    }
+
+    /* Delete 로깅 */
+    @AfterReturning(pointcut = "execution(* com.aolda.itda.service.routing.*Service.*delete*(..))")
+    public void deleteLogging(JoinPoint joinPoint) {
+
+        /* 사용자 조회 */
+        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+        Map<String, String> tmp = (Map<String, String>) request.getSession().getAttribute("user");
+        User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
+                () -> new CustomException(ErrorCode.NOT_FOUND_USER)
+        );
+
+        /* 삭제된 엔티티 조회 */
+        Object[] args = joinPoint.getArgs();
+
+        Long id = (Long) args[0];
+        Routing routing = routingRepository.findByRoutingIdAndIsDeleted(id, true).orElse(null);
+
+        /* 로그 메세지 작성 */
+        String description = "name: " + routing.getName() + "\n"
+                + "domain: " + routing.getDomain() + "\n"
+                + "ip: " + routing.getInstanceIp() + "\n"
+                + "ip: " + routing.getInstancePort() + "\n"
+                + "certificateId: " + routing.getCertificate().getCertificateId() + "\n"
+                + "caching: " + routing.getCaching() + "\n";
+
+        /* 로그 엔티티 저장 */
+        logRepository.save(Log.builder()
+                .user(user)
+                .objectType(ObjectType.FORWARDING)
+                .objectId(routing.getRoutingId())
+                .action(Action.UPDATE)
+                .projectId(routing.getProjectId())
+                .description(description)
+                .build());
+    }
+
+    /* Update(edit) 로깅 */
+    @Around("execution(* com.aolda.itda.service.forwarding.*Service.*edit*(..))")
+    public Object editLogging(ProceedingJoinPoint joinPoint) throws Throwable {
+
+        /* 사용자 조회 */
+        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+        Map<String, String> tmp = (Map<String, String>) request.getSession().getAttribute("user");
+        User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
+                () -> new CustomException(ErrorCode.NOT_FOUND_USER)
+        );
+
+        /* 변경 전 엔티티 조회 */
+        Object[] args = joinPoint.getArgs();
+
+        Long id = (Long) args[0];
+        Routing old = routingRepository.findByRoutingIdAndIsDeleted(id, false).orElse(null);
+        if (old != null) {
+            entityManager.detach(old);
+        }
+
+        /* 메소드 진행 */
+        Object result = joinPoint.proceed();
+
+        /* 변경 후 엔티티 조회 */
+        Routing newObj = routingRepository.findByRoutingIdAndIsDeleted(id, false).orElse(null);
+
+        /* 로그 메세지 작성 */
+        String description = "name: " + old.getName() + (old.getName().equals(newObj.getName()) ? "" : (" -> " + newObj.getName())) + "\n"
+                + "domain: " + old.getDomain() + (old.getDomain().equals(newObj.getDomain()) ? "" : (" -> " + newObj.getDomain())) + "\n"
+                + "ip: " + (old.getInstanceIp().equals(newObj.getInstanceIp()) ? "" : (" -> " + newObj.getInstanceIp())) + "\n"
+                + "port: " + (old.getInstancePort().equals(newObj.getInstancePort()) ? "" : (" -> " + newObj.getInstancePort())) + "\n"
+                + "certificateId: " + (old.getCertificate().getCertificateId() == newObj.getCertificate().getCertificateId() ? "" : (" -> " + newObj.getCertificate().getCertificateId()))
+                + "certificateId: " + (old.getCaching() == newObj.getCaching() ? "" : (" -> " + newObj.getCaching()));
+
+        /* 로그 엔티티 저장 */
+        logRepository.save(Log.builder()
+                .user(user)
+                .objectType(ObjectType.FORWARDING)
+                .objectId(newObj.getRoutingId())
+                .action(Action.UPDATE)
+                .projectId(newObj.getProjectId())
+                .description(description)
+                .build());
+        return result;
+    }
+}
diff --git a/src/main/java/com/aolda/itda/config/AuthInterceptor.java b/src/main/java/com/aolda/itda/config/AuthInterceptor.java
index 83c3821..3a95f8a 100644
--- a/src/main/java/com/aolda/itda/config/AuthInterceptor.java
+++ b/src/main/java/com/aolda/itda/config/AuthInterceptor.java
@@ -1,10 +1,9 @@
 package com.aolda.itda.config;
 
-import com.aolda.itda.dto.auth.ProjectIdAndNameDTO;
+import com.aolda.itda.dto.auth.IdAndNameDTO;
 import com.aolda.itda.exception.CustomException;
 import com.aolda.itda.exception.ErrorCode;
 import com.aolda.itda.service.AuthService;
-import com.fasterxml.jackson.core.JsonProcessingException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.RequiredArgsConstructor;
@@ -14,7 +13,6 @@ import org.springframework.web.servlet.HandlerInterceptor;
 
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 
 @RequiredArgsConstructor
 @Component
@@ -59,9 +57,10 @@ public class AuthInterceptor implements HandlerInterceptor {
 
         /* 프로젝트 리스트 조회 */
         List<String> projects = authService.getProjectsWithUser(Map.of("id", userId, "token", token))
-                        .stream().map(ProjectIdAndNameDTO::getId)
+                        .stream().map(IdAndNameDTO::getId)
                         .toList();
         request.setAttribute("projects", projects);
+        request.setAttribute("user", Map.of("id", userId, "token", token));
         return true;
 
     }
diff --git a/src/main/java/com/aolda/itda/config/WebConfig.java b/src/main/java/com/aolda/itda/config/WebConfig.java
index 71e134b..e3893ae 100644
--- a/src/main/java/com/aolda/itda/config/WebConfig.java
+++ b/src/main/java/com/aolda/itda/config/WebConfig.java
@@ -1,6 +1,9 @@
 package com.aolda.itda.config;
 
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import jakarta.persistence.EntityManager;
 import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.servlet.config.annotation.CorsRegistry;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
@@ -25,10 +28,15 @@ public class WebConfig implements WebMvcConfigurer {
 
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
-        String[] excludeAuth = {"/error", "/api/auth/*", "/api/*" };
+        String[] excludeAuth = {"/error", "/api/auth/*" };
 
         registry.addInterceptor(authInterceptor)
                 .addPathPatterns("/**")
                 .excludePathPatterns(excludeAuth);
     }
+
+    @Bean
+    public JPAQueryFactory jpaQueryFactory(EntityManager em) {
+        return new JPAQueryFactory(em);
+    }
 }
diff --git a/src/main/java/com/aolda/itda/controller/log/LogController.java b/src/main/java/com/aolda/itda/controller/log/LogController.java
new file mode 100644
index 0000000..0d4bfcf
--- /dev/null
+++ b/src/main/java/com/aolda/itda/controller/log/LogController.java
@@ -0,0 +1,41 @@
+package com.aolda.itda.controller.log;
+
+import com.aolda.itda.service.log.LogService;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api")
+@RequiredArgsConstructor
+public class LogController {
+
+    private final LogService logService;
+
+    @GetMapping("/log")
+    public ResponseEntity<Object> view(@RequestParam Long logId, HttpServletRequest request) {
+        return ResponseEntity.ok(logService.getLog(logId, (List<String>) request.getAttribute("projects")));
+    }
+
+    @GetMapping("/logs")
+    public ResponseEntity<Object> lists(@RequestParam(required = false) String projectId
+                                        ,@RequestParam(required = false) String type
+                                        ,@RequestParam(required = false) String username
+                                        ,@RequestParam(required = false) String action
+                                        ,@RequestParam(defaultValue = "false") boolean isASC
+                                        ,@PageableDefault(size = 10) Pageable pageable
+                                        ,HttpServletRequest request) {
+        return ResponseEntity.ok(logService.getLogs(projectId, type, username, action, isASC, pageable,
+                (Map<String, String>) request.getAttribute("user")));
+    }
+
+}
diff --git a/src/main/java/com/aolda/itda/dto/PageResp.java b/src/main/java/com/aolda/itda/dto/PageResp.java
index 49a2a6c..7079600 100644
--- a/src/main/java/com/aolda/itda/dto/PageResp.java
+++ b/src/main/java/com/aolda/itda/dto/PageResp.java
@@ -15,7 +15,7 @@ import java.util.List;
 @JsonInclude(JsonInclude.Include.NON_NULL)
 public class PageResp<T> {
     private Integer totalPages;
-    private Integer totalElements;
+    private Long totalElements;
     private Integer size;
     private List<T> contents;
     private Boolean first;
diff --git a/src/main/java/com/aolda/itda/dto/auth/IdAndNameDTO.java b/src/main/java/com/aolda/itda/dto/auth/IdAndNameDTO.java
new file mode 100644
index 0000000..5918105
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/auth/IdAndNameDTO.java
@@ -0,0 +1,24 @@
+package com.aolda.itda.dto.auth;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.querydsl.core.annotations.QueryProjection;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class IdAndNameDTO {
+
+    private String id;
+    private String name;
+
+    @QueryProjection
+    public IdAndNameDTO(String id, String name) {
+        this.id = id;
+        this.name = name;
+    }
+}
diff --git a/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java b/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java
index 33ba32f..a647b05 100644
--- a/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java
+++ b/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java
@@ -13,5 +13,5 @@ import java.util.List;
 @Builder
 public class LoginResponseDTO {
     private Boolean isAdmin;
-    private List<ProjectIdAndNameDTO> projects;
+    private List<IdAndNameDTO> projects;
 }
diff --git a/src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java b/src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java
deleted file mode 100644
index 1ca894d..0000000
--- a/src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.aolda.itda.dto.auth;
-
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-@Getter
-@NoArgsConstructor
-@AllArgsConstructor
-@Builder
-public class ProjectIdAndNameDTO {
-
-    private String id;
-    private String name;
-}
diff --git a/src/main/java/com/aolda/itda/dto/log/LogDTO.java b/src/main/java/com/aolda/itda/dto/log/LogDTO.java
new file mode 100644
index 0000000..5895455
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/log/LogDTO.java
@@ -0,0 +1,38 @@
+package com.aolda.itda.dto.log;
+
+import com.aolda.itda.dto.auth.IdAndNameDTO;
+import com.aolda.itda.entity.log.Action;
+import com.aolda.itda.entity.log.ObjectType;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.querydsl.core.annotations.QueryProjection;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class LogDTO {
+    private Long id;
+    private IdAndNameDTO user;
+    private Action action;
+    private ObjectType type;
+    private Long objectId;
+    private String description;
+    private LocalDateTime createdAt;
+
+    @QueryProjection
+    public LogDTO(Long id, IdAndNameDTO user, Action action, ObjectType type, Long objectId, String description, LocalDateTime createdAt) {
+        this.id = id;
+        this.user = user;
+        this.action = action;
+        this.type = type;
+        this.objectId = objectId;
+        this.description = description;
+        this.createdAt = createdAt;
+    }
+}
diff --git a/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java b/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java
index 4460eb7..6cfa1fd 100644
--- a/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java
+++ b/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java
@@ -1,5 +1,6 @@
 package com.aolda.itda.entity;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import jakarta.persistence.Column;
 import jakarta.persistence.EntityListeners;
 import jakarta.persistence.MappedSuperclass;
@@ -17,10 +18,12 @@ public abstract class BaseTimeEntity {
 
     @CreatedDate
     @Column(updatable = false)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
     private LocalDateTime createdAt;
 
     @LastModifiedDate
     @Column(name = "updated_at")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
     private LocalDateTime updatedAt;
 
 }
diff --git a/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java
index b0c8d34..86f46b7 100644
--- a/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java
+++ b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java
@@ -4,10 +4,7 @@ import com.aolda.itda.dto.forwarding.ForwardingDTO;
 import com.aolda.itda.entity.BaseTimeEntity;
 import com.aolda.itda.entity.user.User;
 import jakarta.persistence.*;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
+import lombok.*;
 
 @Entity
 @Table(name = "forwarding")
@@ -15,6 +12,7 @@ import lombok.NoArgsConstructor;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
+@ToString
 public class Forwarding extends BaseTimeEntity {
 
     @Id
@@ -40,6 +38,18 @@ public class Forwarding extends BaseTimeEntity {
 
     private String name;
 
+    public Forwarding(Forwarding forwarding) {
+        this.forwardingId = forwarding.getForwardingId();
+        this.user = forwarding.getUser();
+        this.projectId = forwarding.getProjectId();
+        this.serverIp = forwarding.getServerIp();
+        this.serverPort = forwarding.getServerPort();
+        this.instanceIp = forwarding.getInstanceIp();
+        this.instancePort = forwarding.getInstancePort();
+        this.isDeleted = forwarding.getIsDeleted();
+        this.name = forwarding.getName();
+    }
+
     public ForwardingDTO toForwardingDTO() {
         return ForwardingDTO.builder()
                 .id(forwardingId)
diff --git a/src/main/java/com/aolda/itda/entity/log/Log.java b/src/main/java/com/aolda/itda/entity/log/Log.java
index fa78a66..373a836 100644
--- a/src/main/java/com/aolda/itda/entity/log/Log.java
+++ b/src/main/java/com/aolda/itda/entity/log/Log.java
@@ -1,5 +1,7 @@
 package com.aolda.itda.entity.log;
 
+import com.aolda.itda.dto.auth.IdAndNameDTO;
+import com.aolda.itda.dto.log.LogDTO;
 import com.aolda.itda.entity.BaseTimeEntity;
 import com.aolda.itda.entity.user.User;
 import jakarta.persistence.*;
@@ -37,4 +39,16 @@ public class Log extends BaseTimeEntity {
 
     private String description;
 
+    public LogDTO toLogDTO() {
+        return LogDTO.builder()
+                .id(logId)
+                .user(IdAndNameDTO.builder().id(user.getKeystoneId()).name(user.getKeystoneUsername()).build())
+                .action(action)
+                .type(objectType)
+                .objectId(objectId)
+                .description(description)
+                .createdAt(getCreatedAt())
+                .build();
+    }
+
 }
diff --git a/src/main/java/com/aolda/itda/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java
index 6eea5fb..3e4b0be 100644
--- a/src/main/java/com/aolda/itda/exception/ErrorCode.java
+++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java
@@ -10,6 +10,7 @@ public enum ErrorCode {
 
     // User
     INVALID_USER_INFO(HttpStatus.BAD_REQUEST, "잘못된 회원 정보입니다"),
+    NOT_FOUND_USER(HttpStatus.BAD_REQUEST, "존재하지 않는 사용자입니다"),
     UNAUTHORIZED_USER(HttpStatus.BAD_REQUEST, "권한이 없는 사용자입니다"),
 
     // Token
@@ -20,6 +21,9 @@ public enum ErrorCode {
 
     // Routing
     NOT_FOUND_ROUTING(HttpStatus.BAD_REQUEST, "라우팅 파일이 존재하지 않습니다"),
+    
+    // Routing
+    NOT_FOUND_LOG(HttpStatus.BAD_REQUEST, "로그가 존재하지 않습니다"),
 
     // Certificate
     NOT_FOUND_CERTIFICATE(HttpStatus.BAD_REQUEST, "SSL 인증서가 존재하지 않습니다"),
diff --git a/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java b/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java
new file mode 100644
index 0000000..3ec962b
--- /dev/null
+++ b/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java
@@ -0,0 +1,104 @@
+package com.aolda.itda.repository.log;
+
+import com.aolda.itda.dto.PageResp;
+import com.aolda.itda.dto.auth.QIdAndNameDTO;
+import com.aolda.itda.dto.log.LogDTO;
+import com.aolda.itda.dto.log.QLogDTO;
+import com.aolda.itda.entity.log.Action;
+import com.aolda.itda.entity.log.ObjectType;
+import com.querydsl.core.BooleanBuilder;
+import com.querydsl.jpa.impl.JPAQuery;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.support.PageableExecutionUtils;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+import static com.aolda.itda.entity.log.QLog.*;
+import static com.aolda.itda.entity.user.QUser.*;
+
+@Repository
+@RequiredArgsConstructor
+public class LogQueryDSL {
+
+    private final JPAQueryFactory jpaQueryFactory;
+
+    /* log 목록 반환 */
+    public PageResp<LogDTO> getLogs(String projectId, String type,
+                                    String username, String action, Boolean isASC, Pageable pageable) {
+
+        List<LogDTO> content = jpaQueryFactory
+                .select(new QLogDTO(
+                        log.logId,
+                        new QIdAndNameDTO(user.keystoneId, user.keystoneUsername),
+                        log.action,
+                        log.objectType,
+                        log.objectId,
+                        log.description,
+                        log.createdAt
+                )).from(log)
+                .join(log.user, user).on(log.user.eq(user))
+                .where(getFilter(projectId, type, username, action))
+                .orderBy(isASC ? log.logId.asc() : log.logId.desc())
+                .offset(pageable.getOffset())
+                .limit(pageable.getPageSize())
+                .fetch();
+
+        JPAQuery<Long> cnt = jpaQueryFactory
+                .select(log.count())
+                .from(log)
+                .where(getFilter(projectId, type, username, action));
+
+        Page<LogDTO> page = PageableExecutionUtils.getPage(content, pageable, cnt::fetchOne);
+
+        return PageResp.<LogDTO>builder()
+                .contents(page.getContent())
+                .first(page.isFirst())
+                .last(page.isLast())
+                .size(page.getSize())
+                .totalPages(page.getTotalPages())
+                .totalElements(page.getTotalElements())
+                .build();
+    }
+
+    /* Where 필터 */
+    private BooleanBuilder getFilter(String projectId, String type,
+                                     String username, String action) {
+        BooleanBuilder builder = new BooleanBuilder();
+
+        /* 프로젝트 조건 */
+        if (projectId != null) {
+            builder.and(log.projectId.eq(projectId));
+        }
+
+        /* 오브젝트 타입 조건 ( 기본 : ROUTING ) */
+        if (type.equals("certificate")) {
+            builder.and(log.objectType.eq(ObjectType.CERTIFICATE));
+        }
+        else if (type.equals("forwarding")) {
+            builder.and(log.objectType.eq(ObjectType.FORWARDING));
+        }
+        else {
+            builder.and(log.objectType.eq(ObjectType.ROUTING));
+        }
+
+        /* 사용자 ID 조건 */
+        if (username != null) {
+            builder.and(log.user.keystoneUsername.eq(username));
+        }
+
+        /* CUD 조건 */
+        if (action.equals("create")) {
+            builder.and(log.action.eq(Action.CREATE));
+        } else if (action.equals("update")) {
+            builder.and(log.action.eq(Action.UPDATE));
+        } else if (action.equals("delete")) {
+            builder.and(log.action.eq(Action.DELETE));
+        }
+
+        return builder;
+    }
+}
diff --git a/src/main/java/com/aolda/itda/repository/log/LogRepository.java b/src/main/java/com/aolda/itda/repository/log/LogRepository.java
new file mode 100644
index 0000000..de0a769
--- /dev/null
+++ b/src/main/java/com/aolda/itda/repository/log/LogRepository.java
@@ -0,0 +1,10 @@
+package com.aolda.itda.repository.log;
+
+import com.aolda.itda.entity.log.Log;
+import com.aolda.itda.entity.routing.Routing;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface LogRepository extends JpaRepository<Log, Long> {
+}
diff --git a/src/main/java/com/aolda/itda/repository/user/UserRepository.java b/src/main/java/com/aolda/itda/repository/user/UserRepository.java
new file mode 100644
index 0000000..44cae68
--- /dev/null
+++ b/src/main/java/com/aolda/itda/repository/user/UserRepository.java
@@ -0,0 +1,11 @@
+package com.aolda.itda.repository.user;
+
+import com.aolda.itda.entity.user.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface UserRepository extends JpaRepository<User, Long> {
+    Optional<User> findByKeystoneId(String keystoneId);
+    Optional<User> findByKeystoneUsername(String keystoneUsername);
+}
diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java
index cbc157f..be6d764 100644
--- a/src/main/java/com/aolda/itda/service/AuthService.java
+++ b/src/main/java/com/aolda/itda/service/AuthService.java
@@ -2,9 +2,11 @@ package com.aolda.itda.service;
 
 import com.aolda.itda.dto.auth.LoginRequestDTO;
 import com.aolda.itda.dto.auth.LoginResponseDTO;
-import com.aolda.itda.dto.auth.ProjectIdAndNameDTO;
+import com.aolda.itda.dto.auth.IdAndNameDTO;
+import com.aolda.itda.entity.user.User;
 import com.aolda.itda.exception.CustomException;
 import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.repository.user.UserRepository;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -31,6 +33,7 @@ public class AuthService {
     private String adminPassword;
     private final RestTemplate restTemplate = new RestTemplate();
     private final ObjectMapper objectMapper = new ObjectMapper();
+    private final UserRepository userRepository;
 
     // 사용자 로그인 후 토큰 발행 및 Role 반환
     public LoginResponseDTO userLogin(HttpServletResponse response, LoginRequestDTO loginRequestDTO) throws JsonProcessingException {
@@ -44,6 +47,13 @@ public class AuthService {
             throw new CustomException(ErrorCode.INVALID_USER_INFO);
         }
 
+
+        User entity = userRepository.findByKeystoneUsername(userId).orElse(null);
+        if (entity == null) {
+            userRepository.save(User.builder().keystoneId(validateTokenAndGetUserId(token)).
+                    keystoneUsername(userId).build());
+        }
+
         response.addHeader("X-Subject-Token", systemToken != null ? systemToken : token);
         return LoginResponseDTO.builder()
                 .isAdmin(systemToken != null)
@@ -239,7 +249,7 @@ public class AuthService {
     }
 
     // 특정 사용자의 참여 프로젝트 반환
-    public List<ProjectIdAndNameDTO> getProjectsWithUser(Map<String, String> user) throws JsonProcessingException {
+    public List<IdAndNameDTO> getProjectsWithUser(Map<String, String> user) throws JsonProcessingException {
         String userId = user.get("id");
         String token = user.get("token");
         if (userId == null || token == null) {
@@ -257,12 +267,12 @@ public class AuthService {
         JsonNode node = objectMapper.readTree(res.getBody());
         ArrayNode arrayNode = (ArrayNode) node.get("projects");
 
-        List<ProjectIdAndNameDTO> lists = new ArrayList<>();
+        List<IdAndNameDTO> lists = new ArrayList<>();
 
         for (JsonNode assignment : arrayNode) {
             String projectId = assignment.path("id").asText();
             String projectName = assignment.path("name").asText();
-            lists.add(new ProjectIdAndNameDTO(projectId, projectName));
+            lists.add(new IdAndNameDTO(projectId, projectName));
         }
         return lists;
     }
@@ -284,12 +294,12 @@ public class AuthService {
     }
 
     public void validateProjectAuth(List<String> projects, String projectId) {
-        if (!projects.contains(projectId)) {
+        if (projects != null && !projects.contains(projectId)) {
             throw new CustomException(ErrorCode.UNAUTHORIZED_USER);
         }
     }
 
-    private Boolean isAdmin(Map<String, String> user) throws JsonProcessingException {
+    public Boolean isAdmin(Map<String, String> user) throws JsonProcessingException {
         String url = keystone + "/role_assignments?user.id=" + user.get("id") + "&scope.system&include_names";
         HttpHeaders headers = new HttpHeaders();
         headers.set("X-Auth-Token", user.get("token"));
diff --git a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
index 441090b..85c1ec3 100644
--- a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
+++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
@@ -1,7 +1,6 @@
 package com.aolda.itda.service.forwarding;
 
 import com.aolda.itda.dto.PageResp;
-import com.aolda.itda.dto.auth.ProjectIdAndNameDTO;
 import com.aolda.itda.dto.forwarding.ForwardingDTO;
 import com.aolda.itda.entity.forwarding.Forwarding;
 import com.aolda.itda.exception.CustomException;
@@ -9,20 +8,14 @@ import com.aolda.itda.exception.ErrorCode;
 import com.aolda.itda.repository.forwarding.ForwardingRepository;
 import com.aolda.itda.service.AuthService;
 import com.aolda.itda.template.ForwardingTemplate;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import jakarta.servlet.http.HttpServletRequest;
 import jakarta.validation.ConstraintViolation;
 import jakarta.validation.Validation;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
-import org.springframework.web.client.HttpClientErrorException;
 import org.springframework.web.client.HttpServerErrorException;
-import org.springframework.web.client.RestClientException;
 import org.springframework.web.client.RestTemplate;
 
 import java.io.BufferedWriter;
@@ -34,7 +27,6 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
 import java.util.List;
-import java.util.Map;
 
 @Service
 @Transactional
@@ -71,7 +63,7 @@ public class ForwardingService {
     }
 
     /* 포트포워딩 생성 */
-    public void createForwarding(String projectId, ForwardingDTO dto) {
+    public ForwardingDTO createForwarding(String projectId, ForwardingDTO dto) {
 
         /* 입력 DTO 검증 */
         validateDTO(dto);
@@ -163,7 +155,7 @@ public class ForwardingService {
             }
             throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
         }
-
+        return forwarding.toForwardingDTO();
     }
 
     /* 포트포워딩 정보 수정 */
diff --git a/src/main/java/com/aolda/itda/service/log/LogService.java b/src/main/java/com/aolda/itda/service/log/LogService.java
new file mode 100644
index 0000000..b33cc2e
--- /dev/null
+++ b/src/main/java/com/aolda/itda/service/log/LogService.java
@@ -0,0 +1,57 @@
+package com.aolda.itda.service.log;
+
+import com.aolda.itda.dto.PageResp;
+import com.aolda.itda.dto.log.LogDTO;
+import com.aolda.itda.entity.log.Log;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.repository.log.LogRepository;
+import com.aolda.itda.repository.log.LogQueryDSL;
+import com.aolda.itda.service.AuthService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Map;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+@Slf4j
+public class LogService {
+
+    private final LogRepository logRepository;
+    private final LogQueryDSL logQueryDSL;
+    private final AuthService authService;
+
+    /* CUD 로그 조회 */
+    public LogDTO getLog(Long logId, List<String> projects) {
+        Log log = logRepository.findById(logId)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_LOG));
+
+        /* 프로젝트 권한 검증 */
+        authService.validateProjectAuth(projects, log.getProjectId());
+
+        return log.toLogDTO();
+    }
+
+    /* CUD 로그 목록 조회 */
+    public PageResp<LogDTO> getLogs(String projectId, String type, String username, String action, Boolean isASC,
+                                    Pageable pageable, Map<String, String> user) {
+
+        if (projectId == null) {
+            try {
+                if(!authService.isAdmin(user)) throw new CustomException(ErrorCode.UNAUTHORIZED_USER);
+            }
+            catch (Exception e) {
+                throw new CustomException(ErrorCode.UNAUTHORIZED_USER);
+            }
+        }
+
+        return logQueryDSL.getLogs(projectId, type, username, action, isASC, pageable);
+    }
+
+}
diff --git a/src/main/java/com/aolda/itda/service/routing/RoutingService.java b/src/main/java/com/aolda/itda/service/routing/RoutingService.java
index eb3777b..7b30c91 100644
--- a/src/main/java/com/aolda/itda/service/routing/RoutingService.java
+++ b/src/main/java/com/aolda/itda/service/routing/RoutingService.java
@@ -54,7 +54,6 @@ public class RoutingService {
 
     /* Routing 목록 조회 */
     public PageResp<RoutingDTO> getRoutings(String projectId) {
-        // project id 확인 필요
         return PageResp.<RoutingDTO>builder()
                 .contents(routingRepository.findByProjectIdAndIsDeleted(projectId, false)
                         .stream()
@@ -63,7 +62,7 @@ public class RoutingService {
     }
 
     /* Routing 생성 */
-    public void createRouting(String projectId, RoutingDTO dto) {
+    public RoutingDTO createRouting(String projectId, RoutingDTO dto) {
         /* 입력 DTO 검증 */
         validateDTO(dto);
 
@@ -157,6 +156,7 @@ public class RoutingService {
             throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
         }
 
+        return routing.toRoutingDTO();
     }
 
     /* Routing 수정 */
@@ -167,9 +167,6 @@ public class RoutingService {
         /* 프로젝트 권한 검증 */
         authService.validateProjectAuth(projects, routing.getProjectId());
 
-        /* 입력 DTO 검증 */
-        validateDTO(dto);
-
         /* 중복 검증 */
         if (dto.getDomain() != null && routingRepository.existsByDomainAndIsDeleted(dto.getDomain(), false)) {
             throw new CustomException(ErrorCode.DUPLICATED_DOMAIN_NAME);
@@ -182,7 +179,9 @@ public class RoutingService {
 
         /* 파일 수정 */
         routing.edit(dto, certificate);
-        String content = routingTemplate.getRouting(routing.toRoutingDTO(), certificate == null ? null : certificate.formatDomain());
+        RoutingDTO tmp = routing.toRoutingDTO();
+        if (tmp.getCertificateId() == null) tmp.setCertificateId( (long) -1);
+        String content = routingTemplate.getRouting(tmp, certificate == null ? null : certificate.formatDomain());
         String confPath = "/data/nginx/proxy_host/" + routing.getRoutingId() + ".conf";
         File file = new File(confPath);
         if (!file.exists()) {
-- 
GitLab


From 8207b91fbc687d2f13658ca6a7904d93855eea59 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Mon, 24 Mar 2025 19:00:48 +0900
Subject: [PATCH 23/41] =?UTF-8?q?fix:=20DB=20=EC=97=B0=EA=B4=80=EA=B4=80?=
 =?UTF-8?q?=EA=B3=84=20=EC=88=98=EC=A0=95,=20=EB=A1=9C=EA=B7=B8=20?=
 =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=EA=B0=80=20=EC=9E=98=EB=AA=BB?=
 =?UTF-8?q?=EB=90=98=EC=96=B4=EC=9E=88=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../itda/aspect/ForwardingLogAspect.java      | 15 +++---
 .../aolda/itda/aspect/RoutingLogAspect.java   | 51 ++++++++++++-------
 .../itda/entity/certificate/Certificate.java  |  2 +-
 .../itda/entity/forwarding/Forwarding.java    |  5 --
 .../java/com/aolda/itda/entity/log/Log.java   |  2 +-
 .../aolda/itda/entity/routing/Routing.java    |  6 +--
 .../itda/repository/log/LogQueryDSL.java      | 30 +++++------
 .../com/aolda/itda/service/AuthService.java   |  2 +-
 8 files changed, 58 insertions(+), 55 deletions(-)

diff --git a/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java b/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java
index f515ab6..d50c2fe 100644
--- a/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java
+++ b/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java
@@ -23,6 +23,7 @@ import org.aspectj.lang.annotation.AfterReturning;
 import org.aspectj.lang.annotation.Around;
 import org.aspectj.lang.annotation.Aspect;
 import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.context.request.RequestContextHolder;
 import org.springframework.web.context.request.ServletRequestAttributes;
 
@@ -46,7 +47,7 @@ public class ForwardingLogAspect {
 
         /* 사용자 조회 */
         HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
-        Map<String, String> tmp = (Map<String, String>) request.getSession().getAttribute("user");
+        Map<String, String> tmp = (Map<String, String>) request.getAttribute("user");
         User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
                 () -> new CustomException(ErrorCode.NOT_FOUND_USER)
         );
@@ -65,7 +66,7 @@ public class ForwardingLogAspect {
                 .user(user)
                 .objectType(ObjectType.FORWARDING)
                 .objectId(forwarding.getForwardingId())
-                .action(Action.UPDATE)
+                .action(Action.CREATE)
                 .projectId(forwarding.getProjectId())
                 .description(description)
                 .build());
@@ -77,7 +78,7 @@ public class ForwardingLogAspect {
         
         /* 사용자 조회 */
         HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
-        Map<String, String> tmp = (Map<String, String>) request.getSession().getAttribute("user");
+        Map<String, String> tmp = (Map<String, String>) request.getAttribute("user");
         User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
                 () -> new CustomException(ErrorCode.NOT_FOUND_USER)
         );
@@ -99,7 +100,7 @@ public class ForwardingLogAspect {
                 .user(user)
                 .objectType(ObjectType.FORWARDING)
                 .objectId(forwarding.getForwardingId())
-                .action(Action.UPDATE)
+                .action(Action.DELETE)
                 .projectId(forwarding.getProjectId())
                 .description(description)
                 .build());
@@ -111,7 +112,7 @@ public class ForwardingLogAspect {
         
         /* 사용자 조회 */
         HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
-        Map<String, String> tmp = (Map<String, String>) request.getSession().getAttribute("user");
+        Map<String, String> tmp = (Map<String, String>) request.getAttribute("user");
         User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
                 () -> new CustomException(ErrorCode.NOT_FOUND_USER)
         );
@@ -134,8 +135,8 @@ public class ForwardingLogAspect {
         /* 로그 메세지 작성 */
         String description = "name: " + old.getName() + (old.getName().equals(newObj.getName()) ? "" : (" -> " + newObj.getName())) + "\n"
                 + "serverPort: " + old.getServerPort() + (old.getServerPort().equals(newObj.getServerPort()) ? "" : (" -> " + newObj.getServerPort())) + "\n"
-                + "instanceIp: " + (old.getInstanceIp().equals(newObj.getInstanceIp()) ? "" : (" -> " + newObj.getInstanceIp())) + "\n"
-                + "instancePort: " + (old.getInstancePort().equals(newObj.getInstancePort()) ? "" : (" -> " + newObj.getInstancePort()));
+                + "instanceIp: " + old.getInstanceIp() + (old.getInstanceIp().equals(newObj.getInstanceIp()) ? "" : (" -> " + newObj.getInstanceIp())) + "\n"
+                + "instancePort: " + old.getInstancePort() + (old.getInstancePort().equals(newObj.getInstancePort()) ? "" : (" -> " + newObj.getInstancePort()));
 
         /* 로그 엔티티 저장 */
         logRepository.save(Log.builder()
diff --git a/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java
index c8b052a..a3a67d2 100644
--- a/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java
+++ b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java
@@ -23,6 +23,7 @@ import org.aspectj.lang.annotation.AfterReturning;
 import org.aspectj.lang.annotation.Around;
 import org.aspectj.lang.annotation.Aspect;
 import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.context.request.RequestContextHolder;
 import org.springframework.web.context.request.ServletRequestAttributes;
 
@@ -46,7 +47,7 @@ public class RoutingLogAspect {
 
         /* 사용자 조회 */
         HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
-        Map<String, String> tmp = (Map<String, String>) request.getSession().getAttribute("user");
+        Map<String, String> tmp = (Map<String, String>) request.getAttribute("user");
         User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
                 () -> new CustomException(ErrorCode.NOT_FOUND_USER)
         );
@@ -58,16 +59,16 @@ public class RoutingLogAspect {
         String description = "name: " + routing.getName() + "\n"
                 + "domain: " + routing.getDomain() + "\n"
                 + "ip: " + routing.getInstanceIp() + "\n"
-                + "ip: " + routing.getInstancePort() + "\n"
-                + "certificateId: " + routing.getCertificate().getCertificateId() + "\n"
+                + "port: " + routing.getInstancePort() + "\n"
+                +  (routing.getCertificate() != null ? ("certificateId: " + routing.getCertificate().getCertificateId() + "\n") : "")
                 + "caching: " + routing.getCaching() + "\n";
 
         /* 로그 엔티티 저장 */
         logRepository.save(Log.builder()
                 .user(user)
-                .objectType(ObjectType.FORWARDING)
+                .objectType(ObjectType.ROUTING)
                 .objectId(routing.getRoutingId())
-                .action(Action.UPDATE)
+                .action(Action.CREATE)
                 .projectId(routing.getProjectId())
                 .description(description)
                 .build());
@@ -79,7 +80,7 @@ public class RoutingLogAspect {
 
         /* 사용자 조회 */
         HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
-        Map<String, String> tmp = (Map<String, String>) request.getSession().getAttribute("user");
+        Map<String, String> tmp = (Map<String, String>) request.getAttribute("user");
         User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
                 () -> new CustomException(ErrorCode.NOT_FOUND_USER)
         );
@@ -94,28 +95,28 @@ public class RoutingLogAspect {
         String description = "name: " + routing.getName() + "\n"
                 + "domain: " + routing.getDomain() + "\n"
                 + "ip: " + routing.getInstanceIp() + "\n"
-                + "ip: " + routing.getInstancePort() + "\n"
-                + "certificateId: " + routing.getCertificate().getCertificateId() + "\n"
+                + "port: " + routing.getInstancePort() + "\n"
+                +  (routing.getCertificate() != null ? ("certificateId: " + routing.getCertificate().getCertificateId() + "\n") : "")
                 + "caching: " + routing.getCaching() + "\n";
 
         /* 로그 엔티티 저장 */
         logRepository.save(Log.builder()
                 .user(user)
-                .objectType(ObjectType.FORWARDING)
+                .objectType(ObjectType.ROUTING)
                 .objectId(routing.getRoutingId())
-                .action(Action.UPDATE)
+                .action(Action.DELETE)
                 .projectId(routing.getProjectId())
                 .description(description)
                 .build());
     }
 
     /* Update(edit) 로깅 */
-    @Around("execution(* com.aolda.itda.service.forwarding.*Service.*edit*(..))")
+    @Around("execution(* com.aolda.itda.service.routing.*Service.*edit*(..))")
     public Object editLogging(ProceedingJoinPoint joinPoint) throws Throwable {
 
         /* 사용자 조회 */
         HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
-        Map<String, String> tmp = (Map<String, String>) request.getSession().getAttribute("user");
+        Map<String, String> tmp = (Map<String, String>) request.getAttribute("user");
         User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
                 () -> new CustomException(ErrorCode.NOT_FOUND_USER)
         );
@@ -136,17 +137,29 @@ public class RoutingLogAspect {
         Routing newObj = routingRepository.findByRoutingIdAndIsDeleted(id, false).orElse(null);
 
         /* 로그 메세지 작성 */
-        String description = "name: " + old.getName() + (old.getName().equals(newObj.getName()) ? "" : (" -> " + newObj.getName())) + "\n"
-                + "domain: " + old.getDomain() + (old.getDomain().equals(newObj.getDomain()) ? "" : (" -> " + newObj.getDomain())) + "\n"
-                + "ip: " + (old.getInstanceIp().equals(newObj.getInstanceIp()) ? "" : (" -> " + newObj.getInstanceIp())) + "\n"
-                + "port: " + (old.getInstancePort().equals(newObj.getInstancePort()) ? "" : (" -> " + newObj.getInstancePort())) + "\n"
-                + "certificateId: " + (old.getCertificate().getCertificateId() == newObj.getCertificate().getCertificateId() ? "" : (" -> " + newObj.getCertificate().getCertificateId()))
-                + "certificateId: " + (old.getCaching() == newObj.getCaching() ? "" : (" -> " + newObj.getCaching()));
+        String description = "name: " + old.getName() + (old.getName().equals(newObj.getName()) ? "" : " -> " + newObj.getName()) + "\n"
+                + "domain: " + old.getDomain() + (old.getDomain().equals(newObj.getDomain()) ? "" : " -> " + newObj.getDomain()) + "\n"
+                + "ip: " + old.getInstanceIp() + (old.getInstanceIp().equals(newObj.getInstanceIp()) ? "" : " -> " + newObj.getInstanceIp()) + "\n"
+                + "port: " + old.getInstancePort() + (old.getInstancePort().equals(newObj.getInstancePort()) ? "" : " -> " + newObj.getInstancePort()) + "\n";
+        if (old.getCertificate() == null) {
+            if (newObj.getCertificate() != null) {
+                description = description + "certificateId: null -> " + newObj.getCertificate().getCertificateId() + "\n";
+            }
+        }
+        else {
+            if (newObj.getCertificate() == null) {
+                description = description + "certificateId: " + old.getCertificate().getCertificateId() + " -> null\n";
+            }
+            else {
+                description = description + "certificateId: " + old.getCertificate().getCertificateId() + " -> " + newObj.getCertificate().getCertificateId() + "\n";
+            }
+        }
+        description = description + "caching: " + (old.getCaching() == newObj.getCaching() ? newObj.getCaching() : (" -> " + newObj.getCaching()));
 
         /* 로그 엔티티 저장 */
         logRepository.save(Log.builder()
                 .user(user)
-                .objectType(ObjectType.FORWARDING)
+                .objectType(ObjectType.ROUTING)
                 .objectId(newObj.getRoutingId())
                 .action(Action.UPDATE)
                 .projectId(newObj.getProjectId())
diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
index 188f05e..a23cf85 100644
--- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
+++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
@@ -23,7 +23,7 @@ public class Certificate extends BaseTimeEntity {
     @Column(nullable = false)
     private Long certificateId;
 
-    @OneToOne
+    @ManyToOne
     @JoinColumn(nullable = false, name = "user_id")
     private User user;
 
diff --git a/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java
index 86f46b7..0e5e505 100644
--- a/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java
+++ b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java
@@ -20,10 +20,6 @@ public class Forwarding extends BaseTimeEntity {
     @Column(nullable = false)
     private Long forwardingId;
 
-    @OneToOne
-    @JoinColumn(name = "user_id")
-    private User user;
-
     private String projectId;
 
     private String serverIp;
@@ -40,7 +36,6 @@ public class Forwarding extends BaseTimeEntity {
 
     public Forwarding(Forwarding forwarding) {
         this.forwardingId = forwarding.getForwardingId();
-        this.user = forwarding.getUser();
         this.projectId = forwarding.getProjectId();
         this.serverIp = forwarding.getServerIp();
         this.serverPort = forwarding.getServerPort();
diff --git a/src/main/java/com/aolda/itda/entity/log/Log.java b/src/main/java/com/aolda/itda/entity/log/Log.java
index 373a836..9832ab2 100644
--- a/src/main/java/com/aolda/itda/entity/log/Log.java
+++ b/src/main/java/com/aolda/itda/entity/log/Log.java
@@ -23,7 +23,7 @@ public class Log extends BaseTimeEntity {
     @Column(nullable = false)
     private Long logId;
 
-    @OneToOne
+    @ManyToOne
     @JoinColumn(name = "user_id", nullable = false)
     private User user;
 
diff --git a/src/main/java/com/aolda/itda/entity/routing/Routing.java b/src/main/java/com/aolda/itda/entity/routing/Routing.java
index 39b7716..de13d82 100644
--- a/src/main/java/com/aolda/itda/entity/routing/Routing.java
+++ b/src/main/java/com/aolda/itda/entity/routing/Routing.java
@@ -24,11 +24,7 @@ public class Routing extends BaseTimeEntity {
     @Column(nullable = false)
     private Long routingId;
 
-    @OneToOne
-    @JoinColumn(name = "user_id")
-    private User user;
-
-    @OneToOne
+    @ManyToOne
     @JoinColumn(name = "certificate_id")
     private Certificate certificate;
 
diff --git a/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java b/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java
index 3ec962b..ceba4b2 100644
--- a/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java
+++ b/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java
@@ -74,31 +74,29 @@ public class LogQueryDSL {
             builder.and(log.projectId.eq(projectId));
         }
 
-        /* 오브젝트 타입 조건 ( 기본 : ROUTING ) */
-        if (type.equals("certificate")) {
-            builder.and(log.objectType.eq(ObjectType.CERTIFICATE));
-        }
-        else if (type.equals("forwarding")) {
-            builder.and(log.objectType.eq(ObjectType.FORWARDING));
-        }
-        else {
-            builder.and(log.objectType.eq(ObjectType.ROUTING));
+        /* 오브젝트 타입 조건 */
+        if (type != null) {
+            switch (type) {
+                case "certificate" -> builder.and(log.objectType.eq(ObjectType.CERTIFICATE));
+                case "forwarding" -> builder.and(log.objectType.eq(ObjectType.FORWARDING));
+                case "routing" -> builder.and(log.objectType.eq(ObjectType.ROUTING));
+            }
         }
 
+
         /* 사용자 ID 조건 */
         if (username != null) {
             builder.and(log.user.keystoneUsername.eq(username));
         }
 
         /* CUD 조건 */
-        if (action.equals("create")) {
-            builder.and(log.action.eq(Action.CREATE));
-        } else if (action.equals("update")) {
-            builder.and(log.action.eq(Action.UPDATE));
-        } else if (action.equals("delete")) {
-            builder.and(log.action.eq(Action.DELETE));
+        if (action != null) {
+            switch (action) {
+                case "create" -> builder.and(log.action.eq(Action.CREATE));
+                case "update" -> builder.and(log.action.eq(Action.UPDATE));
+                case "delete" -> builder.and(log.action.eq(Action.DELETE));
+            }
         }
-
         return builder;
     }
 }
diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java
index be6d764..80c53d3 100644
--- a/src/main/java/com/aolda/itda/service/AuthService.java
+++ b/src/main/java/com/aolda/itda/service/AuthService.java
@@ -51,7 +51,7 @@ public class AuthService {
         User entity = userRepository.findByKeystoneUsername(userId).orElse(null);
         if (entity == null) {
             userRepository.save(User.builder().keystoneId(validateTokenAndGetUserId(token)).
-                    keystoneUsername(userId).build());
+                    keystoneUsername(loginRequestDTO.getId()).build());
         }
 
         response.addHeader("X-Subject-Token", systemToken != null ? systemToken : token);
-- 
GitLab


From c516dbbf1dfd07b8df7ec027170688d9262e9074 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=ED=95=9C=EB=8F=99=ED=98=84?= <hando1220@ajou.ac.kr>
Date: Tue, 25 Mar 2025 14:15:47 +0900
Subject: [PATCH 24/41] =?UTF-8?q?chore:=20lego=20=EB=B0=94=EC=9D=B4?=
 =?UTF-8?q?=EB=84=88=EB=A6=AC=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=9D=B4?=
 =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=9A=A9=EB=9F=89=20=EA=B0=9C=EC=84=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 Dockerfile | 17 +++++++++++++++--
 1 file changed, 15 insertions(+), 2 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index 16f6160..3742c4d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,10 +1,23 @@
 FROM gradle:jdk21 AS build
+
+WORKDIR /tmp
+RUN wget -O lego.tar.gz "https://github.com/go-acme/lego/releases/download/v4.22.2/lego_v4.22.2_linux_amd64.tar.gz" && tar -xzf lego.tar.gz && rm -f lego.tar.gz
+
 WORKDIR /home/gradle/project
-COPY --chown=gradle:gradle . .
+
+COPY --chown=gradle:gradle build.gradle settings.gradle .
+RUN gradle dependencies --no-daemon
+
+COPY --chown=gradle:gradle src ./src
 RUN gradle clean bootJar --no-daemon
 
-FROM openjdk:21-jdk-slim
+FROM eclipse-temurin:21-jre-alpine
+
+COPY --from=build /tmp/lego /usr/local/bin/lego
+RUN chmod +x /usr/local/bin/lego
+
 WORKDIR /app
 COPY --from=build /home/gradle/project/build/libs/*.jar app.jar
+
 EXPOSE 8080
 ENTRYPOINT ["java", "-jar", "app.jar"]
-- 
GitLab


From 1c4faf35c8da14efff5a198ce993dcc526751ff0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Wed, 26 Mar 2025 17:57:28 +0900
Subject: [PATCH 25/41] =?UTF-8?q?feat:=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?=
 =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=B6=9C=EB=A0=A5=20=EB=B0=8F=20=EC=A0=80?=
 =?UTF-8?q?=EC=9E=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/aolda/itda/config/LoggingFilter.java  | 51 +++++++++++
 .../java/com/aolda/itda/entity/user/User.java |  4 +
 .../itda/exception/ApiExceptionHandler.java   |  2 +-
 .../com/aolda/itda/service/AuthService.java   |  8 +-
 .../service/forwarding/ForwardingService.java | 55 +-----------
 .../itda/service/routing/RoutingService.java  | 58 ------------
 src/main/resources/logback-spring.xml         | 90 +++++++++++++++++++
 7 files changed, 153 insertions(+), 115 deletions(-)
 create mode 100644 src/main/java/com/aolda/itda/config/LoggingFilter.java
 create mode 100644 src/main/resources/logback-spring.xml

diff --git a/src/main/java/com/aolda/itda/config/LoggingFilter.java b/src/main/java/com/aolda/itda/config/LoggingFilter.java
new file mode 100644
index 0000000..0dd73b1
--- /dev/null
+++ b/src/main/java/com/aolda/itda/config/LoggingFilter.java
@@ -0,0 +1,51 @@
+package com.aolda.itda.config;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+import org.springframework.web.util.ContentCachingRequestWrapper;
+
+import java.io.IOException;
+
+@Component
+public class LoggingFilter extends OncePerRequestFilter {
+
+    private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class);
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+            throws ServletException, IOException {
+
+        // Request Body를 읽을 수 있도록 래핑
+        ContentCachingRequestWrapper cachingRequest = new ContentCachingRequestWrapper(request);
+        System.out.println("필터 적용");
+        filterChain.doFilter(cachingRequest, response);
+
+        // 로그 기록
+        logRequest(cachingRequest);
+        System.out.println("왜 안돼ㅐ");
+    }
+
+    private void logRequest(ContentCachingRequestWrapper request) {
+        System.out.println("되는거 맞아?");
+        String ip = request.getRemoteAddr();
+        String method = request.getMethod();
+        String uri = request.getRequestURI();
+        String queryString = request.getQueryString();
+        String body = getRequestBody(request);
+
+        logger.info("IP: {}, Method: {}, URI: {}, Query Params: {}, Request Body: {}",
+                ip, method, uri, (queryString != null ? queryString : "None"),
+                (!body.isEmpty() ? body : "None"));
+    }
+
+    private String getRequestBody(ContentCachingRequestWrapper request) {
+        byte[] buf = request.getContentAsByteArray();
+        return (buf.length > 0) ? new String(buf) : "";
+    }
+}
diff --git a/src/main/java/com/aolda/itda/entity/user/User.java b/src/main/java/com/aolda/itda/entity/user/User.java
index df006b2..682a50e 100644
--- a/src/main/java/com/aolda/itda/entity/user/User.java
+++ b/src/main/java/com/aolda/itda/entity/user/User.java
@@ -21,4 +21,8 @@ public class User {
 
     private String keystoneUsername;
     private String keystoneId;
+
+    public void changeUsername(String username) {
+        this.keystoneUsername = username;
+    }
 }
diff --git a/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java b/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java
index 4d1f360..8dbb993 100644
--- a/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java
+++ b/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java
@@ -11,7 +11,7 @@ public class ApiExceptionHandler {
 
     @ExceptionHandler(value = CustomException.class)
     public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
-        log.error("[handleCustomException] {} : {}", e.getErrorCode().name(), e.getErrorCode().getMessage());
+        log.error("[handleCustomException] {} : {}, {}", e.getErrorCode().name(), e.getErrorCode().getMessage(), e.getStackTrace());
         return ErrorResponse.fromException(e);
     }
 }
diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java
index 80c53d3..6a25ebc 100644
--- a/src/main/java/com/aolda/itda/service/AuthService.java
+++ b/src/main/java/com/aolda/itda/service/AuthService.java
@@ -48,11 +48,15 @@ public class AuthService {
         }
 
 
-        User entity = userRepository.findByKeystoneUsername(userId).orElse(null);
+        User entity = userRepository.findByKeystoneId(userId).orElse(null);
         if (entity == null) {
-            userRepository.save(User.builder().keystoneId(validateTokenAndGetUserId(token)).
+            userRepository.save(User.builder().keystoneId(userId).
                     keystoneUsername(loginRequestDTO.getId()).build());
         }
+        else if (!entity.getKeystoneUsername().equals(loginRequestDTO.getId())) {
+            entity.changeUsername(loginRequestDTO.getId());
+            userRepository.save(entity);
+        }
 
         response.addHeader("X-Subject-Token", systemToken != null ? systemToken : token);
         return LoginResponseDTO.builder()
diff --git a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
index 85c1ec3..cae38c3 100644
--- a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
+++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
@@ -102,7 +102,6 @@ public class ForwardingService {
                 throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "중복된 포트포워딩 Conf 파일이 존재합니다");
             }
         } catch (IOException e) {
-            e.printStackTrace();
             throw new CustomException(ErrorCode.FAIL_CREATE_CONF);
         }
 
@@ -113,7 +112,6 @@ public class ForwardingService {
             bw.flush();
             bw.close();
         } catch (Exception e) {
-            e.printStackTrace();
             if (file.delete()) {
                 throw new CustomException(ErrorCode.FAIL_DELETE_CONF);
             }
@@ -131,7 +129,6 @@ public class ForwardingService {
             }
             throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
         } catch (Exception e) {
-            log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage());
             if (file.delete()) {
                 throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
             }
@@ -149,7 +146,6 @@ public class ForwardingService {
             }
             throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
         } catch (Exception e) {
-            log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage());
             if (file.delete()) {
                 throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
             }
@@ -168,8 +164,6 @@ public class ForwardingService {
 
         /* 중복 검증 */
         if (dto.getServerPort() != null && forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) {
-            System.out.println(dto.getServerPort());
-            System.out.println(forwarding.getServerPort());
             forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false);
             throw new CustomException(ErrorCode.DUPLICATED_SERVER_PORT);
         }
@@ -179,12 +173,6 @@ public class ForwardingService {
                         dto.getInstanceIp() == null ? forwarding.getInstanceIp() : dto.getInstanceIp()
                 , dto.getInstancePort() == null ? forwarding.getInstancePort() : dto.getInstancePort()
                 , false)) {
-            System.out.println(dto.getInstanceIp());
-            System.out.println(forwarding.getInstanceIp());
-            System.out.println(forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted(
-                    dto.getInstanceIp() == null ? forwarding.getInstanceIp() : dto.getInstanceIp()
-                    , dto.getInstancePort() == null ? forwarding.getInstancePort() : dto.getInstancePort()
-                    , false));
             throw new CustomException(ErrorCode.DUPLICATED_INSTANCE_INFO);
         }
 
@@ -211,7 +199,6 @@ public class ForwardingService {
             bw.flush();
             bw.close();
         } catch (Exception e) {
-            e.printStackTrace();
             throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "포트포워딩 Conf 파일을 수정하지 못했습니다");
         }
 
@@ -219,19 +206,8 @@ public class ForwardingService {
         String url = "http://nginx:8081/nginx-api/test";
         try {
             restTemplate.getForEntity(url, String.class);
-        } catch (HttpServerErrorException.InternalServerError e) {
-            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
-            try {
-                Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
-                        , StandardCopyOption.COPY_ATTRIBUTES);
-                Files.delete(backup);
-            } catch (IOException e1) {
-                throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(포트포워딩 Conf 파일 수정)");
-            }
-
-            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
         } catch (RuntimeException e) {
-            log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage());
+
             try {
                 Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
                         , StandardCopyOption.COPY_ATTRIBUTES);
@@ -246,18 +222,7 @@ public class ForwardingService {
         url = "http://nginx:8081/nginx-api/reload";
         try {
             restTemplate.getForEntity(url, String.class);
-        } catch (HttpServerErrorException.InternalServerError e) {
-            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
-            try {
-                Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
-                        , StandardCopyOption.COPY_ATTRIBUTES);
-                Files.delete(backup);
-            } catch (IOException e1) {
-                throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(포트포워딩 Conf 파일 수정)");
-            }
-            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
         } catch (RuntimeException e) {
-            log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage());
             try {
                 Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
                 , StandardCopyOption.COPY_ATTRIBUTES);
@@ -293,16 +258,7 @@ public class ForwardingService {
         String url = "http://nginx:8081/nginx-api/test";
         try {
             restTemplate.getForEntity(url, String.class);
-        } catch (HttpServerErrorException.InternalServerError e) {
-            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
-            try {
-                Files.move(Paths.get(deletePath), Paths.get(confPath));
-            } catch (IOException e1) {
-                throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(포트포워딩 Conf 삭제)");
-            }
-            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
         } catch (Exception e) {
-            log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage());
             try {
                 Files.move(Paths.get(deletePath), Paths.get(confPath));
             } catch (IOException e1) {
@@ -315,16 +271,7 @@ public class ForwardingService {
         url = "http://nginx:8081/nginx-api/reload";
         try {
             restTemplate.getForEntity(url, String.class);
-        } catch (HttpServerErrorException.InternalServerError e) {
-            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
-            try {
-                Files.move(Paths.get(deletePath), Paths.get(confPath));
-            } catch (IOException e1) {
-                throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(포트포워딩 Conf 삭제)");
-            }
-            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
         } catch (Exception e) {
-            log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage());
             try {
                 Files.move(Paths.get(deletePath), Paths.get(confPath));
             } catch (IOException e1) {
diff --git a/src/main/java/com/aolda/itda/service/routing/RoutingService.java b/src/main/java/com/aolda/itda/service/routing/RoutingService.java
index 7b30c91..769278d 100644
--- a/src/main/java/com/aolda/itda/service/routing/RoutingService.java
+++ b/src/main/java/com/aolda/itda/service/routing/RoutingService.java
@@ -102,7 +102,6 @@ public class RoutingService {
                 throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "중복된 라우팅 Conf 파일이 존재합니다");
             }
         } catch (IOException e) {
-            e.printStackTrace();
             throw new CustomException(ErrorCode.FAIL_CREATE_CONF);
         }
 
@@ -113,7 +112,6 @@ public class RoutingService {
             bw.flush();
             bw.close();
         } catch (Exception e) {
-            e.printStackTrace();
             if (file.delete()) {
                 throw new CustomException(ErrorCode.FAIL_DELETE_CONF);
             }
@@ -124,14 +122,7 @@ public class RoutingService {
         String url = "http://nginx:8081/nginx-api/test";
         try {
             restTemplate.getForEntity(url, String.class);
-        } catch (HttpServerErrorException.InternalServerError e) {
-            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
-            if (file.delete()) {
-                throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
-            }
-            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
         } catch (Exception e) {
-            log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage());
             if (file.delete()) {
                 throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
             }
@@ -142,14 +133,7 @@ public class RoutingService {
         url = "http://nginx:8081/nginx-api/reload";
         try {
             restTemplate.getForEntity(url, String.class);
-        } catch (HttpServerErrorException.InternalServerError e) {
-            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
-            if (file.delete()) {
-                throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
-            }
-            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
         } catch (Exception e) {
-            log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage());
             if (file.delete()) {
                 throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
             }
@@ -199,7 +183,6 @@ public class RoutingService {
             bw.flush();
             bw.close();
         } catch (Exception e) {
-            e.printStackTrace();
             throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "라우팅 Conf 파일을 수정하지 못했습니다");
         }
 
@@ -207,19 +190,7 @@ public class RoutingService {
         String url = "http://nginx:8081/nginx-api/test";
         try {
             restTemplate.getForEntity(url, String.class);
-        } catch (HttpServerErrorException.InternalServerError e) {
-            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
-            try {
-                Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
-                        , StandardCopyOption.COPY_ATTRIBUTES);
-                Files.delete(backup);
-            } catch (IOException e1) {
-                throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(라우팅 Conf 파일 수정)");
-            }
-
-            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
         } catch (RuntimeException e) {
-            log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage());
             try {
                 Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
                         , StandardCopyOption.COPY_ATTRIBUTES);
@@ -234,18 +205,7 @@ public class RoutingService {
         url = "http://nginx:8081/nginx-api/reload";
         try {
             restTemplate.getForEntity(url, String.class);
-        } catch (HttpServerErrorException.InternalServerError e) {
-            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
-            try {
-                Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
-                        , StandardCopyOption.COPY_ATTRIBUTES);
-                Files.delete(backup);
-            } catch (IOException e1) {
-                throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(라우팅 Conf 파일 수정)");
-            }
-            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
         } catch (RuntimeException e) {
-            log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage());
             try {
                 Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
                         , StandardCopyOption.COPY_ATTRIBUTES);
@@ -281,16 +241,7 @@ public class RoutingService {
         String url = "http://nginx:8081/nginx-api/test";
         try {
             restTemplate.getForEntity(url, String.class);
-        } catch (HttpServerErrorException.InternalServerError e) {
-            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
-            try {
-                Files.move(Paths.get(deletePath), Paths.get(confPath));
-            } catch (IOException e1) {
-                throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(라우팅 Conf 삭제)");
-            }
-            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
         } catch (Exception e) {
-            log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage());
             try {
                 Files.move(Paths.get(deletePath), Paths.get(confPath));
             } catch (IOException e1) {
@@ -303,16 +254,7 @@ public class RoutingService {
         url = "http://nginx:8081/nginx-api/reload";
         try {
             restTemplate.getForEntity(url, String.class);
-        } catch (HttpServerErrorException.InternalServerError e) {
-            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
-            try {
-                Files.move(Paths.get(deletePath), Paths.get(confPath));
-            } catch (IOException e1) {
-                throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(라우팅 Conf 삭제)");
-            }
-            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
         } catch (Exception e) {
-            log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage());
             try {
                 Files.move(Paths.get(deletePath), Paths.get(confPath));
             } catch (IOException e1) {
diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml
new file mode 100644
index 0000000..a8acbee
--- /dev/null
+++ b/src/main/resources/logback-spring.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+
+    <property name="MAX_FILE_SIZE" value="10MB" />
+    <property name="TOTAL_SIZE" value="1GB" />
+    <property name="MAX_HISTORY" value="30" />
+
+    <!-- 콘솔에 출력할 로그 형식 -->
+    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <!-- INFO 로그 파일 저장 (1개당 10MB, 5개까지 유지, 이후 압축) -->
+    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>/data/logs/info.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>/data/logs/info.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <maxHistory>${MAX_HISTORY}</maxHistory>
+            <totalSizeCap>${TOTAL_SIZE}</totalSizeCap>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+        </rollingPolicy>
+        <encoder>
+            <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+        <!-- INFO 레벨만 허용 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>INFO</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- WARN 로그 파일 저장 -->
+    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>/data/logs/warn.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>/data/logs/warn.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <maxHistory>${MAX_HISTORY}</maxHistory>
+            <totalSizeCap>${TOTAL_SIZE}</totalSizeCap>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+        </rollingPolicy>
+        <encoder>
+            <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+        <!-- WARN 레벨만 허용 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>WARN</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- ERROR 로그 파일 저장 -->
+    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>/data/logs/error.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>/data/logs/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <maxHistory>${MAX_HISTORY}</maxHistory>
+            <totalSizeCap>${TOTAL_SIZE}</totalSizeCap>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+        </rollingPolicy>
+        <encoder>
+            <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+        <!-- ERROR 레벨만 허용 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <logger name="com.aolda.itda" additivity="false">
+        <!-- 각 Appender 참조 (필터는 Appender 내부에 정의됨) -->
+        <appender-ref ref="INFO_FILE"/>
+        <appender-ref ref="WARN_FILE"/>
+        <appender-ref ref="ERROR_FILE"/>
+        <!-- 콘솔 출력 -->
+        <appender-ref ref="CONSOLE"/>
+    </logger>
+
+</configuration>
\ No newline at end of file
-- 
GitLab


From a09cf30c2b6077352b617d462a5fee0aaa5fb368 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Wed, 26 Mar 2025 21:46:11 +0900
Subject: [PATCH 26/41] =?UTF-8?q?feat:=20DB=20=ED=95=84=EB=93=9C=20?=
 =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20=EC=8B=9C?=
 =?UTF-8?q?=EA=B0=84=20=EC=96=91=EC=8B=9D=20=EC=84=A4=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/aolda/itda/dto/forwarding/ForwardingDTO.java   | 5 +++++
 src/main/java/com/aolda/itda/dto/log/LogDTO.java            | 5 +++++
 src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java    | 3 +++
 src/main/java/com/aolda/itda/entity/BaseTimeEntity.java     | 4 ++--
 .../java/com/aolda/itda/entity/certificate/Certificate.java | 4 ++++
 .../java/com/aolda/itda/entity/forwarding/Forwarding.java   | 6 ++++++
 src/main/java/com/aolda/itda/entity/log/Log.java            | 4 ++++
 src/main/java/com/aolda/itda/entity/routing/Routing.java    | 5 +++++
 8 files changed, 34 insertions(+), 2 deletions(-)

diff --git a/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java b/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java
index 3302d0a..be003b6 100644
--- a/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java
+++ b/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java
@@ -1,5 +1,6 @@
 package com.aolda.itda.dto.forwarding;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.Pattern;
@@ -40,6 +41,10 @@ public class ForwardingDTO {
 
     @NotBlank(message = "name 값이 존재하지 않습니다")
     private String name;
+
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
     private LocalDateTime createdAt;
+
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
     private LocalDateTime updatedAt;
 }
diff --git a/src/main/java/com/aolda/itda/dto/log/LogDTO.java b/src/main/java/com/aolda/itda/dto/log/LogDTO.java
index 5895455..9946028 100644
--- a/src/main/java/com/aolda/itda/dto/log/LogDTO.java
+++ b/src/main/java/com/aolda/itda/dto/log/LogDTO.java
@@ -3,6 +3,7 @@ package com.aolda.itda.dto.log;
 import com.aolda.itda.dto.auth.IdAndNameDTO;
 import com.aolda.itda.entity.log.Action;
 import com.aolda.itda.entity.log.ObjectType;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.querydsl.core.annotations.QueryProjection;
 import lombok.AllArgsConstructor;
@@ -22,7 +23,11 @@ public class LogDTO {
     private Action action;
     private ObjectType type;
     private Long objectId;
+
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
     private String description;
+
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
     private LocalDateTime createdAt;
 
     @QueryProjection
diff --git a/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java b/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java
index 6bc48d8..985056e 100644
--- a/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java
+++ b/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java
@@ -1,5 +1,6 @@
 package com.aolda.itda.dto.routing;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
@@ -28,8 +29,10 @@ public class RoutingDTO {
     @NotNull
     private Long certificateId;
 
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
     private LocalDateTime createdAt;
 
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
     private LocalDateTime updatedAt;
 
     @NotNull
diff --git a/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java b/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java
index 6cfa1fd..b62af84 100644
--- a/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java
+++ b/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java
@@ -17,12 +17,12 @@ import java.time.LocalDateTime;
 public abstract class BaseTimeEntity {
 
     @CreatedDate
-    @Column(updatable = false)
+    @Column(updatable = false, columnDefinition = "DATETIME")
     @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
     private LocalDateTime createdAt;
 
     @LastModifiedDate
-    @Column(name = "updated_at")
+    @Column(name = "updated_at", columnDefinition = "DATETIME")
     @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
     private LocalDateTime updatedAt;
 
diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
index a23cf85..85b1f29 100644
--- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
+++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
@@ -27,10 +27,13 @@ public class Certificate extends BaseTimeEntity {
     @JoinColumn(nullable = false, name = "user_id")
     private User user;
 
+    @Column(length = 64)
     private String projectId;
 
+    @Column(length = 64)
     private String domain;
 
+    @Column(length = 64)
     private String email;
 
     private LocalDateTime expiredAt;
@@ -40,6 +43,7 @@ public class Certificate extends BaseTimeEntity {
 
     private Boolean isDeleted;
 
+    @Column(length = 256)
     private String description;
 
     public String formatDomain() {
diff --git a/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java
index 0e5e505..0ba96d6 100644
--- a/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java
+++ b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java
@@ -20,18 +20,24 @@ public class Forwarding extends BaseTimeEntity {
     @Column(nullable = false)
     private Long forwardingId;
 
+    @Column(length = 64)
     private String projectId;
 
+    @Column(length = 32)
     private String serverIp;
 
+    @Column(length = 8)
     private String serverPort;
 
+    @Column(length = 32)
     private String instanceIp;
 
+    @Column(length = 8)
     private String instancePort;
 
     private Boolean isDeleted;
 
+    @Column(length = 256)
     private String name;
 
     public Forwarding(Forwarding forwarding) {
diff --git a/src/main/java/com/aolda/itda/entity/log/Log.java b/src/main/java/com/aolda/itda/entity/log/Log.java
index 9832ab2..6bf6074 100644
--- a/src/main/java/com/aolda/itda/entity/log/Log.java
+++ b/src/main/java/com/aolda/itda/entity/log/Log.java
@@ -27,16 +27,20 @@ public class Log extends BaseTimeEntity {
     @JoinColumn(name = "user_id", nullable = false)
     private User user;
 
+    @Column(length = 64)
     private String projectId;
 
     @Enumerated(EnumType.STRING)
     private ObjectType objectType;
 
+    @Column(length = 64)
     private Long objectId;
 
     @Enumerated(EnumType.STRING)
     private Action action;
 
+    @Lob
+    @Column(length = 1024)
     private String description;
 
     public LogDTO toLogDTO() {
diff --git a/src/main/java/com/aolda/itda/entity/routing/Routing.java b/src/main/java/com/aolda/itda/entity/routing/Routing.java
index de13d82..94ecb55 100644
--- a/src/main/java/com/aolda/itda/entity/routing/Routing.java
+++ b/src/main/java/com/aolda/itda/entity/routing/Routing.java
@@ -28,18 +28,23 @@ public class Routing extends BaseTimeEntity {
     @JoinColumn(name = "certificate_id")
     private Certificate certificate;
 
+    @Column(length = 64)
     private String projectId;
 
+    @Column(length = 64)
     private String domain;
 
+    @Column(length = 32)
     private String instanceIp;
 
+    @Column(length = 8)
     private String instancePort;
 
     private Boolean isDeleted;
 
     private Boolean caching;
 
+    @Column(length = 256)
     private String name;
 
     public RoutingDTO toRoutingDTO() {
-- 
GitLab


From 62f400dffe4db2c570926c9a831617ed14a560e6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Wed, 26 Mar 2025 22:39:01 +0900
Subject: [PATCH 27/41] =?UTF-8?q?feat:=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?=
 =?UTF-8?q?=EC=96=B4=EB=93=9C=EB=AF=BC=20=ED=94=84=EB=A1=9C=EC=A0=9D?=
 =?UTF-8?q?=ED=8A=B8=20=EC=A0=91=EA=B7=BC=20=EA=B6=8C=ED=95=9C=20=EB=B6=80?=
 =?UTF-8?q?=EC=97=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../aolda/itda/config/AuthInterceptor.java    | 15 ++++++++---
 .../com/aolda/itda/config/LoggingFilter.java  |  3 ---
 .../com/aolda/itda/service/AuthService.java   | 25 +++++++++++++++++++
 3 files changed, 37 insertions(+), 6 deletions(-)

diff --git a/src/main/java/com/aolda/itda/config/AuthInterceptor.java b/src/main/java/com/aolda/itda/config/AuthInterceptor.java
index 3a95f8a..b0767e3 100644
--- a/src/main/java/com/aolda/itda/config/AuthInterceptor.java
+++ b/src/main/java/com/aolda/itda/config/AuthInterceptor.java
@@ -11,6 +11,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 import org.springframework.web.servlet.HandlerInterceptor;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -56,9 +57,17 @@ public class AuthInterceptor implements HandlerInterceptor {
         }
 
         /* 프로젝트 리스트 조회 */
-        List<String> projects = authService.getProjectsWithUser(Map.of("id", userId, "token", token))
-                        .stream().map(IdAndNameDTO::getId)
-                        .toList();
+        List<String> projects;
+        if (authService.isAdmin(Map.of("id", userId, "token", token))) {
+            projects = authService.getAllProjects(token);
+        }
+
+        else {
+            projects = authService.getProjectsWithUser(Map.of("id", userId, "token", token))
+                    .stream().map(IdAndNameDTO::getId)
+                    .toList();
+        }
+
         request.setAttribute("projects", projects);
         request.setAttribute("user", Map.of("id", userId, "token", token));
         return true;
diff --git a/src/main/java/com/aolda/itda/config/LoggingFilter.java b/src/main/java/com/aolda/itda/config/LoggingFilter.java
index 0dd73b1..49264ec 100644
--- a/src/main/java/com/aolda/itda/config/LoggingFilter.java
+++ b/src/main/java/com/aolda/itda/config/LoggingFilter.java
@@ -23,16 +23,13 @@ public class LoggingFilter extends OncePerRequestFilter {
 
         // Request Body를 읽을 수 있도록 래핑
         ContentCachingRequestWrapper cachingRequest = new ContentCachingRequestWrapper(request);
-        System.out.println("필터 적용");
         filterChain.doFilter(cachingRequest, response);
 
         // 로그 기록
         logRequest(cachingRequest);
-        System.out.println("왜 안돼ㅐ");
     }
 
     private void logRequest(ContentCachingRequestWrapper request) {
-        System.out.println("되는거 맞아?");
         String ip = request.getRemoteAddr();
         String method = request.getMethod();
         String uri = request.getRequestURI();
diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java
index 6a25ebc..8bc6836 100644
--- a/src/main/java/com/aolda/itda/service/AuthService.java
+++ b/src/main/java/com/aolda/itda/service/AuthService.java
@@ -297,6 +297,31 @@ public class AuthService {
 
     }
 
+    public List<String> getAllProjects(String token) throws JsonProcessingException {
+        String url = keystone + "/projects";
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("X-Auth-Token", token);
+        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
+        ResponseEntity<String> res;
+        try {
+            res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
+        } catch (HttpClientErrorException.NotFound e) {
+            throw new CustomException(ErrorCode.INVALID_TOKEN);
+        }
+
+        JsonNode node = objectMapper.readTree(res.getBody());
+        ArrayNode arrayNode = (ArrayNode) node.get("projects");
+
+        List<String> lists = new ArrayList<>();
+
+        for (JsonNode assignment : arrayNode) {
+            lists.add(assignment.path("id").asText());
+        }
+
+        return lists;
+
+    }
+
     public void validateProjectAuth(List<String> projects, String projectId) {
         if (projects != null && !projects.contains(projectId)) {
             throw new CustomException(ErrorCode.UNAUTHORIZED_USER);
-- 
GitLab


From 1b407531c1208ef0f09867426b7cc75eafd20486 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Wed, 26 Mar 2025 23:39:18 +0900
Subject: [PATCH 28/41] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?=
 =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=B0=8F=20=EC=A0=91=EA=B7=BC=20=EA=B0=80?=
 =?UTF-8?q?=EB=8A=A5=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=A1=B0?=
 =?UTF-8?q?=ED=9A=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../aolda/itda/config/AuthInterceptor.java    |  3 +-
 .../itda/controller/main/MainController.java  | 33 ++++++++++
 .../com/aolda/itda/dto/main/MainInfoDTO.java  | 20 ++++++
 .../com/aolda/itda/service/AuthService.java   |  8 ++-
 .../aolda/itda/service/main/MainService.java  | 61 +++++++++++++++++++
 5 files changed, 121 insertions(+), 4 deletions(-)
 create mode 100644 src/main/java/com/aolda/itda/controller/main/MainController.java
 create mode 100644 src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java
 create mode 100644 src/main/java/com/aolda/itda/service/main/MainService.java

diff --git a/src/main/java/com/aolda/itda/config/AuthInterceptor.java b/src/main/java/com/aolda/itda/config/AuthInterceptor.java
index b0767e3..7c9f87f 100644
--- a/src/main/java/com/aolda/itda/config/AuthInterceptor.java
+++ b/src/main/java/com/aolda/itda/config/AuthInterceptor.java
@@ -59,7 +59,8 @@ public class AuthInterceptor implements HandlerInterceptor {
         /* 프로젝트 리스트 조회 */
         List<String> projects;
         if (authService.isAdmin(Map.of("id", userId, "token", token))) {
-            projects = authService.getAllProjects(token);
+            projects = authService.getAllProjects(token).stream().map(IdAndNameDTO::getId)
+                    .toList();
         }
 
         else {
diff --git a/src/main/java/com/aolda/itda/controller/main/MainController.java b/src/main/java/com/aolda/itda/controller/main/MainController.java
new file mode 100644
index 0000000..7b22923
--- /dev/null
+++ b/src/main/java/com/aolda/itda/controller/main/MainController.java
@@ -0,0 +1,33 @@
+package com.aolda.itda.controller.main;
+
+import com.aolda.itda.service.main.MainService;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api")
+@RequiredArgsConstructor
+public class MainController {
+
+    private final MainService mainService;
+
+    @GetMapping("/projects")
+    public ResponseEntity<Object> projects(HttpServletRequest request) throws JsonProcessingException {
+        return ResponseEntity.ok(mainService.getAllProjects((Map<String, String>) request.getSession().getAttribute("user")));
+    }
+
+    @GetMapping("/main")
+    public ResponseEntity<Object> mainInfo(@RequestParam String projectId, HttpServletRequest request) {
+        return ResponseEntity.ok(mainService.getMainInfo(projectId, (List<String>) request.getAttribute("projects")));
+    }
+
+}
diff --git a/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java b/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java
new file mode 100644
index 0000000..6de5fdf
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java
@@ -0,0 +1,20 @@
+package com.aolda.itda.dto.main;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class MainInfoDTO {
+
+    private Long routing;
+    private Long forwarding;
+    private Long certificate;
+
+}
diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java
index 8bc6836..a222e28 100644
--- a/src/main/java/com/aolda/itda/service/AuthService.java
+++ b/src/main/java/com/aolda/itda/service/AuthService.java
@@ -297,7 +297,7 @@ public class AuthService {
 
     }
 
-    public List<String> getAllProjects(String token) throws JsonProcessingException {
+    public List<IdAndNameDTO> getAllProjects(String token) throws JsonProcessingException {
         String url = keystone + "/projects";
         HttpHeaders headers = new HttpHeaders();
         headers.set("X-Auth-Token", token);
@@ -312,10 +312,12 @@ public class AuthService {
         JsonNode node = objectMapper.readTree(res.getBody());
         ArrayNode arrayNode = (ArrayNode) node.get("projects");
 
-        List<String> lists = new ArrayList<>();
+        List<IdAndNameDTO> lists = new ArrayList<>();
 
         for (JsonNode assignment : arrayNode) {
-            lists.add(assignment.path("id").asText());
+            String projectId = assignment.path("id").asText();
+            String projectName = assignment.path("name").asText();
+            lists.add(new IdAndNameDTO(projectId, projectName));
         }
 
         return lists;
diff --git a/src/main/java/com/aolda/itda/service/main/MainService.java b/src/main/java/com/aolda/itda/service/main/MainService.java
new file mode 100644
index 0000000..2c36ac8
--- /dev/null
+++ b/src/main/java/com/aolda/itda/service/main/MainService.java
@@ -0,0 +1,61 @@
+package com.aolda.itda.service.main;
+
+import com.aolda.itda.dto.PageResp;
+import com.aolda.itda.dto.auth.IdAndNameDTO;
+import com.aolda.itda.dto.main.MainInfoDTO;
+import com.aolda.itda.repository.certificate.CertificateRepository;
+import com.aolda.itda.repository.forwarding.ForwardingRepository;
+import com.aolda.itda.repository.routing.RoutingRepository;
+import com.aolda.itda.service.AuthService;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Map;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class MainService {
+
+    private final AuthService authService;
+    private final RoutingRepository routingRepository;
+    private final ForwardingRepository forwardingRepository;
+    private final CertificateRepository certificateRepository;
+
+    /* 메인 페이지에 필요한 정보 반환 */
+    public MainInfoDTO getMainInfo(String projectId, List<String> projects) {
+
+        /* 프로젝트 권한 검증 */
+        authService.validateProjectAuth(projects, projectId);
+
+        /* 카운팅 */
+        Long routing = (long) routingRepository.findByProjectIdAndIsDeleted(projectId, false).size();
+        Long forwarding = (long) forwardingRepository.findByProjectIdAndIsDeleted(projectId, false).size();
+        Long certificate = 0L;
+
+        return MainInfoDTO.builder()
+                .routing(routing)
+                .forwarding(forwarding)
+                .certificate(certificate)
+                .build();
+    }
+
+    /* 접근 가능한 프로젝트 조회 */
+    public PageResp<IdAndNameDTO> getAllProjects(Map<String, String> user) throws JsonProcessingException {
+
+        List<IdAndNameDTO> projects;
+        if (authService.isAdmin(user)) {
+            projects = authService.getAllProjects(user.get("token"));
+        }
+
+        else {
+            projects = authService.getProjectsWithUser(user);
+        }
+
+        return PageResp.<IdAndNameDTO>builder()
+                .contents(projects).build();
+    }
+}
-- 
GitLab


From 017ea5c5ad239d906bdb7f85f9290a01dd99e8d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Sat, 29 Mar 2025 11:08:48 +0900
Subject: [PATCH 29/41] =?UTF-8?q?fix:=20=ED=94=84=EB=A1=9C=EC=A0=9D?=
 =?UTF-8?q?=ED=8A=B8=20=EC=97=AD=ED=95=A0=EA=B3=BC=20=EA=B6=8C=ED=95=9C?=
 =?UTF-8?q?=EC=9D=B4=20=EC=9D=BC=EC=B9=98=ED=95=98=EC=A7=80=20=EC=95=8A?=
 =?UTF-8?q?=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/aolda/itda/config/AuthInterceptor.java |  6 +++---
 .../java/com/aolda/itda/service/AuthService.java    | 13 +++++--------
 2 files changed, 8 insertions(+), 11 deletions(-)

diff --git a/src/main/java/com/aolda/itda/config/AuthInterceptor.java b/src/main/java/com/aolda/itda/config/AuthInterceptor.java
index 7c9f87f..fd78fb6 100644
--- a/src/main/java/com/aolda/itda/config/AuthInterceptor.java
+++ b/src/main/java/com/aolda/itda/config/AuthInterceptor.java
@@ -43,10 +43,10 @@ public class AuthInterceptor implements HandlerInterceptor {
         if (projectId != null) {
 
             try {
-                String role = authService.getBestRoleWithinProject(token, projectId).get("role");
-                if (!role.equals("admin")) {
-                    log.error("Unauthorized Token for URI {}: {}", request.getRequestURI(), request.getRemoteAddr());
+                authService.getBestRoleWithinProject(token, projectId).get("role");
+                if (!request.getMethod().equals("GET") && !authService.getBestRoleWithinProject(token, projectId).get("role").equals("admin")) {
                     throw new CustomException(ErrorCode.UNAUTHORIZED_USER, request.getRequestURI());
+
                 }
             } catch (Exception e) {
                 throw new CustomException(ErrorCode.UNAUTHORIZED_USER, request.getRequestURI());
diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java
index a222e28..4c1740e 100644
--- a/src/main/java/com/aolda/itda/service/AuthService.java
+++ b/src/main/java/com/aolda/itda/service/AuthService.java
@@ -96,7 +96,6 @@ public class AuthService {
         try {
             res = restTemplate.postForEntity(url, requestEntity, Map.class);
         } catch (Exception e) {
-            e.printStackTrace();
             throw new CustomException(ErrorCode.INVALID_USER_INFO);
         }
         Map<String, Object> resToken = (Map<String, Object>) res.getBody().get("token");
@@ -140,7 +139,7 @@ public class AuthService {
         try {
             requestEntity = new HttpEntity<>(requestBody, headers);
             res = restTemplate.postForEntity(url, requestEntity, Map.class);
-        } catch (RuntimeException e) {
+        } catch (Exception e) {
             return null;
         }
 
@@ -185,8 +184,7 @@ public class AuthService {
         } catch (HttpClientErrorException.Forbidden e) {
             return unscopedToken;
         }
-        catch (RuntimeException e) {
-            e.printStackTrace();
+        catch (Exception e) {
             throw new CustomException(ErrorCode.INVALID_TOKEN);
         }
 
@@ -290,7 +288,7 @@ public class AuthService {
         ResponseEntity<String> res;
         try {
             res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
-        } catch (HttpClientErrorException.NotFound e) {
+        } catch (Exception e) {
             throw new CustomException(ErrorCode.INVALID_TOKEN);
         }
         return objectMapper.readTree(res.getBody()).path("token").path("user").path("id").asText();
@@ -305,7 +303,7 @@ public class AuthService {
         ResponseEntity<String> res;
         try {
             res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
-        } catch (HttpClientErrorException.NotFound e) {
+        } catch (Exception e) {
             throw new CustomException(ErrorCode.INVALID_TOKEN);
         }
 
@@ -338,8 +336,7 @@ public class AuthService {
         ResponseEntity<String> res;
         try {
             res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
-        } catch (RuntimeException e) {
-            e.printStackTrace();
+        } catch (Exception e) {
             return false;
         }
         JsonNode node = objectMapper.readTree(res.getBody()).path("role_assignments");
-- 
GitLab


From 1b0c96231a33afa111a7016e5289cfbea0930104 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Sat, 29 Mar 2025 11:09:28 +0900
Subject: [PATCH 30/41] =?UTF-8?q?fix:=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20?=
 =?UTF-8?q?=EC=9E=91=EC=97=85=20=EB=A1=9C=EA=B7=B8=EC=97=90=20=EA=B0=9C?=
 =?UTF-8?q?=ED=96=89=EC=9D=B4=20=ED=95=9C=20=EB=B2=88=20=EB=8D=94=20?=
 =?UTF-8?q?=EB=93=A4=EC=96=B4=EA=B0=80=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java
index a3a67d2..97f7de4 100644
--- a/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java
+++ b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java
@@ -61,7 +61,7 @@ public class RoutingLogAspect {
                 + "ip: " + routing.getInstanceIp() + "\n"
                 + "port: " + routing.getInstancePort() + "\n"
                 +  (routing.getCertificate() != null ? ("certificateId: " + routing.getCertificate().getCertificateId() + "\n") : "")
-                + "caching: " + routing.getCaching() + "\n";
+                + "caching: " + routing.getCaching();
 
         /* 로그 엔티티 저장 */
         logRepository.save(Log.builder()
@@ -97,7 +97,7 @@ public class RoutingLogAspect {
                 + "ip: " + routing.getInstanceIp() + "\n"
                 + "port: " + routing.getInstancePort() + "\n"
                 +  (routing.getCertificate() != null ? ("certificateId: " + routing.getCertificate().getCertificateId() + "\n") : "")
-                + "caching: " + routing.getCaching() + "\n";
+                + "caching: " + routing.getCaching();
 
         /* 로그 엔티티 저장 */
         logRepository.save(Log.builder()
-- 
GitLab


From aa911ace917181294680c8cf68ce89ef7b26fb3a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Sat, 29 Mar 2025 11:56:18 +0900
Subject: [PATCH 31/41] =?UTF-8?q?fix:=20enum=20type=20=EC=86=8C=EB=AC=B8?=
 =?UTF-8?q?=EC=9E=90=20=EC=A7=81=EB=A0=AC=ED=99=94=EB=A1=9C=20=EB=B3=80?=
 =?UTF-8?q?=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/aolda/itda/config/LoggingFilter.java  |  96 +++++-----
 .../itda/controller/main/MainController.java  |  66 +++----
 .../com/aolda/itda/dto/main/MainInfoDTO.java  |  40 ++--
 .../itda/entity/certificate/Challenge.java    |  12 +-
 .../com/aolda/itda/entity/log/Action.java     |  12 +-
 .../com/aolda/itda/entity/log/ObjectType.java |  12 +-
 .../aolda/itda/service/main/MainService.java  | 122 ++++++------
 src/main/resources/logback-spring.xml         | 178 +++++++++---------
 8 files changed, 284 insertions(+), 254 deletions(-)

diff --git a/src/main/java/com/aolda/itda/config/LoggingFilter.java b/src/main/java/com/aolda/itda/config/LoggingFilter.java
index 49264ec..f2330bf 100644
--- a/src/main/java/com/aolda/itda/config/LoggingFilter.java
+++ b/src/main/java/com/aolda/itda/config/LoggingFilter.java
@@ -1,48 +1,48 @@
-package com.aolda.itda.config;
-
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.stereotype.Component;
-import org.springframework.web.filter.OncePerRequestFilter;
-import org.springframework.web.util.ContentCachingRequestWrapper;
-
-import java.io.IOException;
-
-@Component
-public class LoggingFilter extends OncePerRequestFilter {
-
-    private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class);
-
-    @Override
-    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
-            throws ServletException, IOException {
-
-        // Request Body를 읽을 수 있도록 래핑
-        ContentCachingRequestWrapper cachingRequest = new ContentCachingRequestWrapper(request);
-        filterChain.doFilter(cachingRequest, response);
-
-        // 로그 기록
-        logRequest(cachingRequest);
-    }
-
-    private void logRequest(ContentCachingRequestWrapper request) {
-        String ip = request.getRemoteAddr();
-        String method = request.getMethod();
-        String uri = request.getRequestURI();
-        String queryString = request.getQueryString();
-        String body = getRequestBody(request);
-
-        logger.info("IP: {}, Method: {}, URI: {}, Query Params: {}, Request Body: {}",
-                ip, method, uri, (queryString != null ? queryString : "None"),
-                (!body.isEmpty() ? body : "None"));
-    }
-
-    private String getRequestBody(ContentCachingRequestWrapper request) {
-        byte[] buf = request.getContentAsByteArray();
-        return (buf.length > 0) ? new String(buf) : "";
-    }
-}
+package com.aolda.itda.config;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+import org.springframework.web.util.ContentCachingRequestWrapper;
+
+import java.io.IOException;
+
+@Component
+public class LoggingFilter extends OncePerRequestFilter {
+
+    private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class);
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+            throws ServletException, IOException {
+
+        // Request Body를 읽을 수 있도록 래핑
+        ContentCachingRequestWrapper cachingRequest = new ContentCachingRequestWrapper(request);
+        filterChain.doFilter(cachingRequest, response);
+
+        // 로그 기록
+        logRequest(cachingRequest);
+    }
+
+    private void logRequest(ContentCachingRequestWrapper request) {
+        String ip = request.getRemoteAddr();
+        String method = request.getMethod();
+        String uri = request.getRequestURI();
+        String queryString = request.getQueryString();
+        String body = getRequestBody(request);
+
+        logger.info("IP: {}, Method: {}, URI: {}, Query Params: {}, Request Body: {}",
+                ip, method, uri, (queryString != null ? queryString : "None"),
+                (!body.isEmpty() ? body : "None"));
+    }
+
+    private String getRequestBody(ContentCachingRequestWrapper request) {
+        byte[] buf = request.getContentAsByteArray();
+        return (buf.length > 0) ? new String(buf) : "";
+    }
+}
diff --git a/src/main/java/com/aolda/itda/controller/main/MainController.java b/src/main/java/com/aolda/itda/controller/main/MainController.java
index 7b22923..371cce5 100644
--- a/src/main/java/com/aolda/itda/controller/main/MainController.java
+++ b/src/main/java/com/aolda/itda/controller/main/MainController.java
@@ -1,33 +1,33 @@
-package com.aolda.itda.controller.main;
-
-import com.aolda.itda.service.main.MainService;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import jakarta.servlet.http.HttpServletRequest;
-import lombok.RequiredArgsConstructor;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-
-import java.util.List;
-import java.util.Map;
-
-@RestController
-@RequestMapping("/api")
-@RequiredArgsConstructor
-public class MainController {
-
-    private final MainService mainService;
-
-    @GetMapping("/projects")
-    public ResponseEntity<Object> projects(HttpServletRequest request) throws JsonProcessingException {
-        return ResponseEntity.ok(mainService.getAllProjects((Map<String, String>) request.getSession().getAttribute("user")));
-    }
-
-    @GetMapping("/main")
-    public ResponseEntity<Object> mainInfo(@RequestParam String projectId, HttpServletRequest request) {
-        return ResponseEntity.ok(mainService.getMainInfo(projectId, (List<String>) request.getAttribute("projects")));
-    }
-
-}
+package com.aolda.itda.controller.main;
+
+import com.aolda.itda.service.main.MainService;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api")
+@RequiredArgsConstructor
+public class MainController {
+
+    private final MainService mainService;
+
+    @GetMapping("/projects")
+    public ResponseEntity<Object> projects(HttpServletRequest request) throws JsonProcessingException {
+        return ResponseEntity.ok(mainService.getAllProjects((Map<String, String>) request.getSession().getAttribute("user")));
+    }
+
+    @GetMapping("/main")
+    public ResponseEntity<Object> mainInfo(@RequestParam String projectId, HttpServletRequest request) {
+        return ResponseEntity.ok(mainService.getMainInfo(projectId, (List<String>) request.getAttribute("projects")));
+    }
+
+}
diff --git a/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java b/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java
index 6de5fdf..cbcdcd5 100644
--- a/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java
+++ b/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java
@@ -1,20 +1,20 @@
-package com.aolda.itda.dto.main;
-
-import com.fasterxml.jackson.annotation.JsonInclude;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-@Builder
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class MainInfoDTO {
-
-    private Long routing;
-    private Long forwarding;
-    private Long certificate;
-
-}
+package com.aolda.itda.dto.main;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class MainInfoDTO {
+
+    private Long routing;
+    private Long forwarding;
+    private Long certificate;
+
+}
diff --git a/src/main/java/com/aolda/itda/entity/certificate/Challenge.java b/src/main/java/com/aolda/itda/entity/certificate/Challenge.java
index 14713bf..41f607e 100644
--- a/src/main/java/com/aolda/itda/entity/certificate/Challenge.java
+++ b/src/main/java/com/aolda/itda/entity/certificate/Challenge.java
@@ -1,5 +1,15 @@
 package com.aolda.itda.entity.certificate;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+@JsonFormat(shape = JsonFormat.Shape.STRING)
 public enum Challenge {
-    HTTP, DNS_CLOUDFLARE
+    HTTP, DNS_CLOUDFLARE;
+
+    @JsonValue
+    @Override
+    public String toString() {
+        return name().toLowerCase();
+    }
 }
diff --git a/src/main/java/com/aolda/itda/entity/log/Action.java b/src/main/java/com/aolda/itda/entity/log/Action.java
index a620f87..448a65e 100644
--- a/src/main/java/com/aolda/itda/entity/log/Action.java
+++ b/src/main/java/com/aolda/itda/entity/log/Action.java
@@ -1,5 +1,15 @@
 package com.aolda.itda.entity.log;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+@JsonFormat(shape = JsonFormat.Shape.STRING)
 public enum Action {
-    CREATE, UPDATE, DELETE
+    CREATE, UPDATE, DELETE;
+
+    @JsonValue
+    @Override
+    public String toString() {
+        return name().toLowerCase();
+    }
 }
diff --git a/src/main/java/com/aolda/itda/entity/log/ObjectType.java b/src/main/java/com/aolda/itda/entity/log/ObjectType.java
index 5310315..c91f279 100644
--- a/src/main/java/com/aolda/itda/entity/log/ObjectType.java
+++ b/src/main/java/com/aolda/itda/entity/log/ObjectType.java
@@ -1,5 +1,15 @@
 package com.aolda.itda.entity.log;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+@JsonFormat(shape = JsonFormat.Shape.STRING)
 public enum ObjectType {
-    ROUTING, CERTIFICATE, FORWARDING
+    ROUTING, CERTIFICATE, FORWARDING;
+
+    @JsonValue
+    @Override
+    public String toString() {
+        return name().toLowerCase();
+    }
 }
diff --git a/src/main/java/com/aolda/itda/service/main/MainService.java b/src/main/java/com/aolda/itda/service/main/MainService.java
index 2c36ac8..8a4345e 100644
--- a/src/main/java/com/aolda/itda/service/main/MainService.java
+++ b/src/main/java/com/aolda/itda/service/main/MainService.java
@@ -1,61 +1,61 @@
-package com.aolda.itda.service.main;
-
-import com.aolda.itda.dto.PageResp;
-import com.aolda.itda.dto.auth.IdAndNameDTO;
-import com.aolda.itda.dto.main.MainInfoDTO;
-import com.aolda.itda.repository.certificate.CertificateRepository;
-import com.aolda.itda.repository.forwarding.ForwardingRepository;
-import com.aolda.itda.repository.routing.RoutingRepository;
-import com.aolda.itda.service.AuthService;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.util.List;
-import java.util.Map;
-
-@Service
-@Transactional
-@RequiredArgsConstructor
-public class MainService {
-
-    private final AuthService authService;
-    private final RoutingRepository routingRepository;
-    private final ForwardingRepository forwardingRepository;
-    private final CertificateRepository certificateRepository;
-
-    /* 메인 페이지에 필요한 정보 반환 */
-    public MainInfoDTO getMainInfo(String projectId, List<String> projects) {
-
-        /* 프로젝트 권한 검증 */
-        authService.validateProjectAuth(projects, projectId);
-
-        /* 카운팅 */
-        Long routing = (long) routingRepository.findByProjectIdAndIsDeleted(projectId, false).size();
-        Long forwarding = (long) forwardingRepository.findByProjectIdAndIsDeleted(projectId, false).size();
-        Long certificate = 0L;
-
-        return MainInfoDTO.builder()
-                .routing(routing)
-                .forwarding(forwarding)
-                .certificate(certificate)
-                .build();
-    }
-
-    /* 접근 가능한 프로젝트 조회 */
-    public PageResp<IdAndNameDTO> getAllProjects(Map<String, String> user) throws JsonProcessingException {
-
-        List<IdAndNameDTO> projects;
-        if (authService.isAdmin(user)) {
-            projects = authService.getAllProjects(user.get("token"));
-        }
-
-        else {
-            projects = authService.getProjectsWithUser(user);
-        }
-
-        return PageResp.<IdAndNameDTO>builder()
-                .contents(projects).build();
-    }
-}
+package com.aolda.itda.service.main;
+
+import com.aolda.itda.dto.PageResp;
+import com.aolda.itda.dto.auth.IdAndNameDTO;
+import com.aolda.itda.dto.main.MainInfoDTO;
+import com.aolda.itda.repository.certificate.CertificateRepository;
+import com.aolda.itda.repository.forwarding.ForwardingRepository;
+import com.aolda.itda.repository.routing.RoutingRepository;
+import com.aolda.itda.service.AuthService;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Map;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class MainService {
+
+    private final AuthService authService;
+    private final RoutingRepository routingRepository;
+    private final ForwardingRepository forwardingRepository;
+    private final CertificateRepository certificateRepository;
+
+    /* 메인 페이지에 필요한 정보 반환 */
+    public MainInfoDTO getMainInfo(String projectId, List<String> projects) {
+
+        /* 프로젝트 권한 검증 */
+        authService.validateProjectAuth(projects, projectId);
+
+        /* 카운팅 */
+        Long routing = (long) routingRepository.findByProjectIdAndIsDeleted(projectId, false).size();
+        Long forwarding = (long) forwardingRepository.findByProjectIdAndIsDeleted(projectId, false).size();
+        Long certificate = 0L;
+
+        return MainInfoDTO.builder()
+                .routing(routing)
+                .forwarding(forwarding)
+                .certificate(certificate)
+                .build();
+    }
+
+    /* 접근 가능한 프로젝트 조회 */
+    public PageResp<IdAndNameDTO> getAllProjects(Map<String, String> user) throws JsonProcessingException {
+
+        List<IdAndNameDTO> projects;
+        if (authService.isAdmin(user)) {
+            projects = authService.getAllProjects(user.get("token"));
+        }
+
+        else {
+            projects = authService.getProjectsWithUser(user);
+        }
+
+        return PageResp.<IdAndNameDTO>builder()
+                .contents(projects).build();
+    }
+}
diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml
index a8acbee..39e9337 100644
--- a/src/main/resources/logback-spring.xml
+++ b/src/main/resources/logback-spring.xml
@@ -1,90 +1,90 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<configuration>
-
-    <property name="MAX_FILE_SIZE" value="10MB" />
-    <property name="TOTAL_SIZE" value="1GB" />
-    <property name="MAX_HISTORY" value="30" />
-
-    <!-- 콘솔에 출력할 로그 형식 -->
-    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
-        <encoder>
-            <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern>
-        </encoder>
-    </appender>
-
-    <!-- INFO 로그 파일 저장 (1개당 10MB, 5개까지 유지, 이후 압축) -->
-    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
-        <file>/data/logs/info.log</file>
-        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <fileNamePattern>/data/logs/info.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
-            <maxHistory>${MAX_HISTORY}</maxHistory>
-            <totalSizeCap>${TOTAL_SIZE}</totalSizeCap>
-            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
-                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
-            </timeBasedFileNamingAndTriggeringPolicy>
-        </rollingPolicy>
-        <encoder>
-            <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern>
-        </encoder>
-        <!-- INFO 레벨만 허용 -->
-        <filter class="ch.qos.logback.classic.filter.LevelFilter">
-            <level>INFO</level>
-            <onMatch>ACCEPT</onMatch>
-            <onMismatch>DENY</onMismatch>
-        </filter>
-    </appender>
-
-    <!-- WARN 로그 파일 저장 -->
-    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
-        <file>/data/logs/warn.log</file>
-        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <fileNamePattern>/data/logs/warn.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
-            <maxHistory>${MAX_HISTORY}</maxHistory>
-            <totalSizeCap>${TOTAL_SIZE}</totalSizeCap>
-            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
-                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
-            </timeBasedFileNamingAndTriggeringPolicy>
-        </rollingPolicy>
-        <encoder>
-            <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern>
-        </encoder>
-        <!-- WARN 레벨만 허용 -->
-        <filter class="ch.qos.logback.classic.filter.LevelFilter">
-            <level>WARN</level>
-            <onMatch>ACCEPT</onMatch>
-            <onMismatch>DENY</onMismatch>
-        </filter>
-    </appender>
-
-    <!-- ERROR 로그 파일 저장 -->
-    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
-        <file>/data/logs/error.log</file>
-        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <fileNamePattern>/data/logs/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
-            <maxHistory>${MAX_HISTORY}</maxHistory>
-            <totalSizeCap>${TOTAL_SIZE}</totalSizeCap>
-            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
-                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
-            </timeBasedFileNamingAndTriggeringPolicy>
-        </rollingPolicy>
-        <encoder>
-            <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern>
-        </encoder>
-        <!-- ERROR 레벨만 허용 -->
-        <filter class="ch.qos.logback.classic.filter.LevelFilter">
-            <level>ERROR</level>
-            <onMatch>ACCEPT</onMatch>
-            <onMismatch>DENY</onMismatch>
-        </filter>
-    </appender>
-
-    <logger name="com.aolda.itda" additivity="false">
-        <!-- 각 Appender 참조 (필터는 Appender 내부에 정의됨) -->
-        <appender-ref ref="INFO_FILE"/>
-        <appender-ref ref="WARN_FILE"/>
-        <appender-ref ref="ERROR_FILE"/>
-        <!-- 콘솔 출력 -->
-        <appender-ref ref="CONSOLE"/>
-    </logger>
-
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+
+    <property name="MAX_FILE_SIZE" value="10MB" />
+    <property name="TOTAL_SIZE" value="1GB" />
+    <property name="MAX_HISTORY" value="30" />
+
+    <!-- 콘솔에 출력할 로그 형식 -->
+    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <!-- INFO 로그 파일 저장 (1개당 10MB, 5개까지 유지, 이후 압축) -->
+    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>/data/logs/info.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>/data/logs/info.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <maxHistory>${MAX_HISTORY}</maxHistory>
+            <totalSizeCap>${TOTAL_SIZE}</totalSizeCap>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+        </rollingPolicy>
+        <encoder>
+            <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+        <!-- INFO 레벨만 허용 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>INFO</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- WARN 로그 파일 저장 -->
+    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>/data/logs/warn.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>/data/logs/warn.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <maxHistory>${MAX_HISTORY}</maxHistory>
+            <totalSizeCap>${TOTAL_SIZE}</totalSizeCap>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+        </rollingPolicy>
+        <encoder>
+            <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+        <!-- WARN 레벨만 허용 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>WARN</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- ERROR 로그 파일 저장 -->
+    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>/data/logs/error.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>/data/logs/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <maxHistory>${MAX_HISTORY}</maxHistory>
+            <totalSizeCap>${TOTAL_SIZE}</totalSizeCap>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+        </rollingPolicy>
+        <encoder>
+            <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+        <!-- ERROR 레벨만 허용 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <logger name="com.aolda.itda" additivity="false">
+        <!-- 각 Appender 참조 (필터는 Appender 내부에 정의됨) -->
+        <appender-ref ref="INFO_FILE"/>
+        <appender-ref ref="WARN_FILE"/>
+        <appender-ref ref="ERROR_FILE"/>
+        <!-- 콘솔 출력 -->
+        <appender-ref ref="CONSOLE"/>
+    </logger>
+
 </configuration>
\ No newline at end of file
-- 
GitLab


From 331b11478b712ac7c3fa1dc2794c3881b53d198d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Sat, 29 Mar 2025 12:04:32 +0900
Subject: [PATCH 32/41] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=20=EC=82=AC?=
 =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EA=B2=80=EC=83=89=EC=9D=84=20=EC=9D=BC?=
 =?UTF-8?q?=EB=B6=80=20=EA=B2=80=EC=83=89=EC=9C=BC=EB=A1=9C=20=EB=B3=80?=
 =?UTF-8?q?=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java b/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java
index ceba4b2..19feb19 100644
--- a/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java
+++ b/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java
@@ -86,7 +86,7 @@ public class LogQueryDSL {
 
         /* 사용자 ID 조건 */
         if (username != null) {
-            builder.and(log.user.keystoneUsername.eq(username));
+            builder.and(log.user.keystoneUsername.contains(username));
         }
 
         /* CUD 조건 */
-- 
GitLab


From 88e8e972827daa6d3b8e7fe1330641e42e8e7a89 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Sat, 29 Mar 2025 13:20:16 +0900
Subject: [PATCH 33/41] =?UTF-8?q?feat:=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?=
 =?UTF-8?q?=ED=9A=8C=EC=97=90=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20?=
 =?UTF-8?q?=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../forwarding/ForwardingController.java      |  5 ++--
 .../controller/routing/RoutingController.java |  5 ++--
 .../forwarding/ForwardingRepository.java      |  4 ++++
 .../repository/routing/RoutingRepository.java |  4 ++++
 .../service/forwarding/ForwardingService.java | 16 ++++++++++---
 .../itda/service/routing/RoutingService.java  | 23 ++++++++++++++++---
 6 files changed, 47 insertions(+), 10 deletions(-)

diff --git a/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java
index 36e6f9a..999707a 100644
--- a/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java
+++ b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java
@@ -30,8 +30,9 @@ public class ForwardingController {
     }
 
     @GetMapping("/forwardings")
-    public ResponseEntity<Object> lists(@RequestParam String projectId) {
-        return ResponseEntity.ok(forwardingService.getForwardings(projectId));
+    public ResponseEntity<Object> lists(@RequestParam String projectId,
+                                        @RequestParam(required = false) String query) {
+        return ResponseEntity.ok(forwardingService.getForwardingsWithSearch(projectId, query));
     }
 
     @PatchMapping("/forwarding")
diff --git a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
index 4b9d551..c96e910 100644
--- a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
+++ b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
@@ -31,8 +31,9 @@ public class RoutingController {
     }
 
     @GetMapping("/routings")
-    public ResponseEntity<Object> lists(@RequestParam String projectId) {
-        return ResponseEntity.ok(routingService.getRoutings(projectId));
+    public ResponseEntity<Object> lists(@RequestParam String projectId,
+                                        @RequestParam(required = false) String query) {
+        return ResponseEntity.ok(routingService.getRoutingsWithSearch(projectId, query));
     }
 
     @PatchMapping("/routing")
diff --git a/src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java b/src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java
index c461762..9a330f0 100644
--- a/src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java
+++ b/src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java
@@ -2,6 +2,7 @@ package com.aolda.itda.repository.forwarding;
 
 import com.aolda.itda.entity.forwarding.Forwarding;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
 
 import java.util.List;
 import java.util.Optional;
@@ -11,4 +12,7 @@ public interface ForwardingRepository extends JpaRepository<Forwarding, Long> {
     Optional<Forwarding> findByForwardingIdAndIsDeleted(Long forwardingId, Boolean isDeleted);
     Boolean existsByInstanceIpAndInstancePortAndIsDeleted(String instanceIp, String instancePort, Boolean isDeleted);
     Boolean existsByServerPortAndIsDeleted(String serverPort, Boolean isDeleted);
+
+    @Query("SELECT f FROM Forwarding f WHERE f.projectId = ?1 AND f.isDeleted = ?3 AND (f.instanceIp LIKE %?2% OR f.serverPort LIKE %?2% OR f.name LIKE %?2%)")
+    List<Forwarding> findWithSearch(String projectId, String query, Boolean isDeleted);
 }
diff --git a/src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java b/src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java
index 046296c..44b88ae 100644
--- a/src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java
+++ b/src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java
@@ -3,6 +3,7 @@ package com.aolda.itda.repository.routing;
 import com.aolda.itda.entity.forwarding.Forwarding;
 import com.aolda.itda.entity.routing.Routing;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
 
 import java.util.List;
 import java.util.Optional;
@@ -11,4 +12,7 @@ public interface RoutingRepository extends JpaRepository<Routing, Long> {
     List<Routing> findByProjectIdAndIsDeleted(String projectId, Boolean isDeleted);
     Optional<Routing> findByRoutingIdAndIsDeleted(Long routingId, Boolean isDeleted);
     Boolean existsByDomainAndIsDeleted(String domain, Boolean isDeleted);
+
+    @Query("SELECT r FROM Routing r WHERE r.projectId = ?1 AND r.isDeleted = ?3 AND (r.domain LIKE %?2% OR r.instanceIp LIKE %?2% OR r.name LIKE %?2%)")
+    List<Routing> findWithSearch(String projectId, String query, Boolean isDeleted);
 }
diff --git a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
index cae38c3..e6fc9d4 100644
--- a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
+++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
@@ -27,6 +27,7 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
 import java.util.List;
+import java.util.regex.Pattern;
 
 @Service
 @Transactional
@@ -52,11 +53,20 @@ public class ForwardingService {
         return forwarding.toForwardingDTO();
     }
 
-    /* 포트포워딩 목록 조회 */
-    public PageResp<ForwardingDTO> getForwardings(String projectId) {
+    /* 포트포워딩 목록 조회 + 검색 */
+    public PageResp<ForwardingDTO> getForwardingsWithSearch(String projectId, String query) {
+
+        /* 입력 검증 */
+        if (query == null || query.isBlank()) {
+            return PageResp.<ForwardingDTO>builder()
+                    .contents(forwardingRepository.findByProjectIdAndIsDeleted(projectId, false)
+                            .stream()
+                            .map(Forwarding::toForwardingDTO)
+                            .toList()).build();
+        }
 
         return PageResp.<ForwardingDTO>builder()
-                .contents(forwardingRepository.findByProjectIdAndIsDeleted(projectId, false)
+                .contents(forwardingRepository.findWithSearch(projectId, query, false)
                         .stream()
                         .map(Forwarding::toForwardingDTO)
                         .toList()).build();
diff --git a/src/main/java/com/aolda/itda/service/routing/RoutingService.java b/src/main/java/com/aolda/itda/service/routing/RoutingService.java
index 769278d..d01eb02 100644
--- a/src/main/java/com/aolda/itda/service/routing/RoutingService.java
+++ b/src/main/java/com/aolda/itda/service/routing/RoutingService.java
@@ -28,6 +28,7 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
 import java.util.List;
+import java.util.regex.Pattern;
 
 @Service
 @Transactional
@@ -52,10 +53,26 @@ public class RoutingService {
         return routing.toRoutingDTO();
     }
 
-    /* Routing 목록 조회 */
-    public PageResp<RoutingDTO> getRoutings(String projectId) {
+    /* Routing 목록 조회 + 검색 */
+    public PageResp<RoutingDTO> getRoutingsWithSearch(String projectId, String query) {
+
+        /* 입력 검증 */
+        if (query == null || query.isBlank()) {
+            return PageResp.<RoutingDTO>builder()
+                    .contents(routingRepository.findByProjectIdAndIsDeleted(projectId, false)
+                            .stream()
+                            .map(Routing::toRoutingDTO)
+                            .toList()).build();
+        }
+
+        /* 도메인 패턴 검증 */
+        String domainPattern = "^(\\*\\.)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$";
+        if (Pattern.matches(domainPattern, query) && query.startsWith("*.")) {
+            query = query.substring(2);
+        }
+
         return PageResp.<RoutingDTO>builder()
-                .contents(routingRepository.findByProjectIdAndIsDeleted(projectId, false)
+                .contents(routingRepository.findWithSearch(projectId, query, false)
                         .stream()
                         .map(Routing::toRoutingDTO)
                         .toList()).build();
-- 
GitLab


From 9662eda4bf9e66027a5cbe31f55ab3721aa676ca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Sat, 29 Mar 2025 13:54:43 +0900
Subject: [PATCH 34/41] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=EC=9D=B8?=
 =?UTF-8?q?=ED=84=B0=EC=85=89=ED=84=B0=EC=97=90=EC=84=9C=20=ED=95=84?=
 =?UTF-8?q?=ED=84=B0=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EB=A1=9C?=
 =?UTF-8?q?=EA=B7=B8=EC=97=90=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A0=95?=
 =?UTF-8?q?=EB=B3=B4=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../{AuthInterceptor.java => AuthFilter.java} | 153 +++++++++---------
 .../com/aolda/itda/config/LoggingFilter.java  |  15 +-
 .../java/com/aolda/itda/config/WebConfig.java |  27 +++-
 3 files changed, 107 insertions(+), 88 deletions(-)
 rename src/main/java/com/aolda/itda/config/{AuthInterceptor.java => AuthFilter.java} (71%)

diff --git a/src/main/java/com/aolda/itda/config/AuthInterceptor.java b/src/main/java/com/aolda/itda/config/AuthFilter.java
similarity index 71%
rename from src/main/java/com/aolda/itda/config/AuthInterceptor.java
rename to src/main/java/com/aolda/itda/config/AuthFilter.java
index fd78fb6..f810c2c 100644
--- a/src/main/java/com/aolda/itda/config/AuthInterceptor.java
+++ b/src/main/java/com/aolda/itda/config/AuthFilter.java
@@ -1,77 +1,76 @@
-package com.aolda.itda.config;
-
-import com.aolda.itda.dto.auth.IdAndNameDTO;
-import com.aolda.itda.exception.CustomException;
-import com.aolda.itda.exception.ErrorCode;
-import com.aolda.itda.service.AuthService;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-import org.springframework.web.servlet.HandlerInterceptor;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-@RequiredArgsConstructor
-@Component
-@Slf4j
-public class AuthInterceptor implements HandlerInterceptor {
-
-    private final AuthService authService;
-
-    @Override
-    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
-        String token = request.getHeader("X-Subject-Token");
-
-        /* 토큰 헤더 검증 */
-        if (token == null || token.isEmpty()) {
-            throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI());
-        }
-
-        /* 유효 토큰 검증 */
-        String userId = authService.validateTokenAndGetUserId(token);
-        if (userId == null) {
-            log.error("Token validation failed for URI {}: {}", request.getRequestURI(), request.getRemoteAddr());
-            throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI());
-        }
-
-        /* 프로젝트 권한 검증 */
-        String projectId = request.getParameter("projectId");
-        if (projectId != null) {
-
-            try {
-                authService.getBestRoleWithinProject(token, projectId).get("role");
-                if (!request.getMethod().equals("GET") && !authService.getBestRoleWithinProject(token, projectId).get("role").equals("admin")) {
-                    throw new CustomException(ErrorCode.UNAUTHORIZED_USER, request.getRequestURI());
-
-                }
-            } catch (Exception e) {
-                throw new CustomException(ErrorCode.UNAUTHORIZED_USER, request.getRequestURI());
-            }
-
-
-
-        }
-
-        /* 프로젝트 리스트 조회 */
-        List<String> projects;
-        if (authService.isAdmin(Map.of("id", userId, "token", token))) {
-            projects = authService.getAllProjects(token).stream().map(IdAndNameDTO::getId)
-                    .toList();
-        }
-
-        else {
-            projects = authService.getProjectsWithUser(Map.of("id", userId, "token", token))
-                    .stream().map(IdAndNameDTO::getId)
-                    .toList();
-        }
-
-        request.setAttribute("projects", projects);
-        request.setAttribute("user", Map.of("id", userId, "token", token));
-        return true;
-
-    }
-}
+package com.aolda.itda.config;
+
+import com.aolda.itda.dto.auth.IdAndNameDTO;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.service.AuthService;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+@RequiredArgsConstructor
+@Component
+@Slf4j
+public class AuthFilter extends OncePerRequestFilter {
+
+    private final AuthService authService;
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+            throws ServletException, IOException {
+
+        if (request.getRequestURI().contains("/api/auth")) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        String token = request.getHeader("X-Subject-Token");
+
+        // 토큰 헤더 검증
+        if (token == null || token.isEmpty()) {
+            throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI());
+        }
+
+        // 유효 토큰 검증
+        String userId = authService.validateTokenAndGetUserId(token);
+        if (userId == null) {
+            log.error("Token validation failed for URI {}: {}", request.getRequestURI(), request.getRemoteAddr());
+            throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI());
+        }
+
+        // 프로젝트 권한 검증
+        String projectId = request.getParameter("projectId");
+        if (projectId != null) {
+            try {
+                authService.getBestRoleWithinProject(token, projectId).get("role");
+                if (!request.getMethod().equals("GET") && !authService.getBestRoleWithinProject(token, projectId).get("role").equals("admin")) {
+                    throw new CustomException(ErrorCode.UNAUTHORIZED_USER, request.getRequestURI());
+                }
+            } catch (Exception e) {
+                throw new CustomException(ErrorCode.UNAUTHORIZED_USER, request.getRequestURI());
+            }
+        }
+
+        // 프로젝트 리스트 조회
+        List<String> projects;
+        if (authService.isAdmin(Map.of("id", userId, "token", token))) {
+            projects = authService.getAllProjects(token).stream().map(IdAndNameDTO::getId).toList();
+        } else {
+            projects = authService.getProjectsWithUser(Map.of("id", userId, "token", token)).stream().map(IdAndNameDTO::getId).toList();
+        }
+
+        request.setAttribute("projects", projects);
+        request.setAttribute("user", Map.of("id", userId, "token", token));
+
+        filterChain.doFilter(request, response);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/aolda/itda/config/LoggingFilter.java b/src/main/java/com/aolda/itda/config/LoggingFilter.java
index f2330bf..8c5c2f0 100644
--- a/src/main/java/com/aolda/itda/config/LoggingFilter.java
+++ b/src/main/java/com/aolda/itda/config/LoggingFilter.java
@@ -1,9 +1,14 @@
 package com.aolda.itda.config;
 
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.service.AuthService;
+import com.fasterxml.jackson.core.JsonProcessingException;
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;
@@ -11,11 +16,14 @@ import org.springframework.web.filter.OncePerRequestFilter;
 import org.springframework.web.util.ContentCachingRequestWrapper;
 
 import java.io.IOException;
+import java.util.Map;
 
 @Component
+@RequiredArgsConstructor
 public class LoggingFilter extends OncePerRequestFilter {
 
     private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class);
+    private final AuthService authService;
 
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
@@ -29,15 +37,16 @@ public class LoggingFilter extends OncePerRequestFilter {
         logRequest(cachingRequest);
     }
 
-    private void logRequest(ContentCachingRequestWrapper request) {
+    private void logRequest(ContentCachingRequestWrapper request) throws JsonProcessingException {
         String ip = request.getRemoteAddr();
         String method = request.getMethod();
         String uri = request.getRequestURI();
         String queryString = request.getQueryString();
         String body = getRequestBody(request);
+        Map<String, String> user = (Map<String, String>) request.getAttribute("user");
 
-        logger.info("IP: {}, Method: {}, URI: {}, Query Params: {}, Request Body: {}",
-                ip, method, uri, (queryString != null ? queryString : "None"),
+        logger.info("IP: {}, Method: {}, URI: {}, Query Params: {}, User: {}, Request Body: {}",
+                ip, method, uri, (queryString != null ? queryString : "None"), (user != null ? user.get("id") : "None"),
                 (!body.isEmpty() ? body : "None"));
     }
 
diff --git a/src/main/java/com/aolda/itda/config/WebConfig.java b/src/main/java/com/aolda/itda/config/WebConfig.java
index e3893ae..a3559c4 100644
--- a/src/main/java/com/aolda/itda/config/WebConfig.java
+++ b/src/main/java/com/aolda/itda/config/WebConfig.java
@@ -3,17 +3,18 @@ package com.aolda.itda.config;
 import com.querydsl.jpa.impl.JPAQueryFactory;
 import jakarta.persistence.EntityManager;
 import lombok.RequiredArgsConstructor;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.servlet.config.annotation.CorsRegistry;
-import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
 @Configuration
 @RequiredArgsConstructor
 public class WebConfig implements WebMvcConfigurer {
 
-    private final AuthInterceptor authInterceptor;
+    private final AuthFilter authFilter;
+    private final LoggingFilter loggingFilter;
 
     @Override
     public void addCorsMappings(CorsRegistry registry) { // 스프링단에서 cors 설정
@@ -26,13 +27,23 @@ public class WebConfig implements WebMvcConfigurer {
         ;
     }
 
-    @Override
-    public void addInterceptors(InterceptorRegistry registry) {
-        String[] excludeAuth = {"/error", "/api/auth/*" };
 
-        registry.addInterceptor(authInterceptor)
-                .addPathPatterns("/**")
-                .excludePathPatterns(excludeAuth);
+    @Bean
+    public FilterRegistrationBean<AuthFilter> authFilterRegistration() {
+        FilterRegistrationBean<AuthFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(authFilter);
+        registrationBean.setOrder(1); // AuthFilter의 순서를 1로 설정
+        registrationBean.addUrlPatterns("/*");
+        return registrationBean;
+    }
+
+    @Bean
+    public FilterRegistrationBean<LoggingFilter> loggingFilterRegistration() {
+        FilterRegistrationBean<LoggingFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(loggingFilter);
+        registrationBean.setOrder(2); // LoggingFilter의 순서를 2로 설정
+        registrationBean.addUrlPatterns("/*");
+        return registrationBean;
     }
 
     @Bean
-- 
GitLab


From 2fedb7cec27a20cd830f8fef5ec6b4f0d07fe7ea Mon Sep 17 00:00:00 2001
From: NaHyun22 <nhle0217@ajou.ac.kr>
Date: Mon, 14 Apr 2025 19:30:39 +0900
Subject: [PATCH 35/41] =?UTF-8?q?fix:=20conflict=20=ED=95=B4=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../certificate/CertificateController.java    |  93 ++++++++++++
 .../itda/dto/certificate/CertificateDTO.java  |  21 +++
 .../itda/entity/certificate/Certificate.java  |   9 ++
 .../certificate/CertificateRepository.java    |  11 ++
 .../certificate/CertificateService.java       | 137 ++++++++++++++++++
 5 files changed, 271 insertions(+)
 create mode 100644 src/main/java/com/aolda/itda/controller/certificate/CertificateController.java
 create mode 100644 src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
 create mode 100644 src/main/java/com/aolda/itda/service/certificate/CertificateService.java

diff --git a/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java
new file mode 100644
index 0000000..b6384c9
--- /dev/null
+++ b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java
@@ -0,0 +1,93 @@
+package com.aolda.itda.controller.certificate;
+
+import com.aolda.itda.dto.certificate.CertificateDTO;
+import com.aolda.itda.service.certificate.CertificateService;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api")
+@RequiredArgsConstructor
+public class CertificateController {
+
+    private final CertificateService certificateService;
+
+    /**
+     * 인증서 생성
+     * POST /api/certificate?projectId=xxx
+     * Body: CertificateDTO
+     */
+    @PostMapping("/certificate")
+    public ResponseEntity<Object> create(@RequestParam String projectId,
+                                         @RequestBody CertificateDTO dto,
+                                         HttpServletRequest request) {
+        certificateService.createCertificate(
+                projectId,
+                dto,
+                (List<String>) request.getAttribute("projects") // Forwarding과 동일하게
+        );
+        return ResponseEntity.ok().build();
+    }
+
+    /**
+     * 인증서 단건 조회
+     * GET /api/certificate?certificateId=xxx
+     */
+    @GetMapping("/certificate")
+    public ResponseEntity<Object> view(@RequestParam Long certificateId,
+                                       HttpServletRequest request) {
+        return ResponseEntity.ok(
+                certificateService.getCertificate(
+                        certificateId,
+                        (List<String>) request.getAttribute("projects")
+                )
+        );
+    }
+
+    /**
+     * 인증서 목록 조회
+     * GET /api/certificates?projectId=xxx
+     */
+    @GetMapping("/certificates")
+    public ResponseEntity<Object> lists(@RequestParam String projectId) {
+        return ResponseEntity.ok(
+                certificateService.getCertificates(projectId)
+        );
+    }
+
+    /**
+     * 인증서 수정
+     * PATCH /api/certificate?certificateId=xxx
+     * Body: CertificateDTO
+     */
+    @PatchMapping("/certificate")
+    public ResponseEntity<Object> edit(@RequestParam Long certificateId,
+                                       @RequestBody CertificateDTO dto,
+                                       HttpServletRequest request) {
+        certificateService.editCertificate(
+                certificateId,
+                dto,
+                (List<String>) request.getAttribute("projects")
+        );
+        return ResponseEntity.ok().build();
+    }
+
+    /**
+     * 인증서 삭제
+     * DELETE /api/certificate?certificateId=xxx
+     */
+    @DeleteMapping("/certificate")
+    public ResponseEntity<Object> delete(@RequestParam Long certificateId,
+                                         HttpServletRequest request) {
+        certificateService.deleteCertificate(
+                certificateId,
+                (List<String>) request.getAttribute("projects")
+        );
+        return ResponseEntity.ok().build();
+    }
+
+}
diff --git a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
new file mode 100644
index 0000000..35aad94
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
@@ -0,0 +1,21 @@
+package com.aolda.itda.dto.certificate;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class CertificateDTO {
+
+    private Long certificateId;
+    private String projectId;
+    private String domain;
+    private LocalDateTime expiredAt;   // 필요 시
+    private Boolean isDeleted;
+    private String description;        // 추가 설명
+}
diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
index 85b1f29..469ea6a 100644
--- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
+++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
@@ -49,4 +49,13 @@ public class Certificate extends BaseTimeEntity {
     public String formatDomain() {
         return domain == null ? null : domain.replace("*", "_");
     }
+
+    public void setIsDeleted(boolean b) {
+    }
+
+    public void setDomain(String domain) {
+    }
+
+    public void setDescription(String description) {
+    }
 }
diff --git a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java
index 392c90b..1ce5c1f 100644
--- a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java
+++ b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java
@@ -1,7 +1,18 @@
 package com.aolda.itda.repository.certificate;
 
 import com.aolda.itda.entity.certificate.Certificate;
+import com.aolda.itda.entity.forwarding.Forwarding;
 import org.springframework.data.jpa.repository.JpaRepository;
 
+import java.util.List;
+import java.util.Optional;
+
 public interface CertificateRepository extends JpaRepository<Certificate, Long> {
+
+    // 단건 조회 (Soft Delete 고려)
+    Optional<Certificate> findByCertificateIdAndIsDeleted(Long certificateId, Boolean isDeleted);
+
+    // 프로젝트별 목록 조회 (Soft Delete 고려)
+    List<Certificate> findByProjectIdAndIsDeleted(String projectId, Boolean isDeleted);
+
 }
diff --git a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
new file mode 100644
index 0000000..5c0dbaf
--- /dev/null
+++ b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
@@ -0,0 +1,137 @@
+package com.aolda.itda.service.certificate;
+
+import com.aolda.itda.dto.PageResp;
+import com.aolda.itda.dto.certificate.CertificateDTO;
+import com.aolda.itda.entity.certificate.Certificate;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.repository.certificate.CertificateRepository;
+import com.aolda.itda.service.AuthService;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+@Slf4j
+public class CertificateService {
+
+    private final CertificateRepository certificateRepository;
+    private final AuthService authService;
+
+    /* 인증서 하나 조회 */
+    public CertificateDTO getCertificate(Long certificateId, List<String> projects) {
+        Certificate certificate = certificateRepository
+                .findByCertificateIdAndIsDeleted(certificateId, false)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
+        // 여기서는 ErrorCode.NOT_FOUND_CERTIFICATE 등 새로운 코드로 교체 가능
+
+        // 프로젝트 권한 검증
+        authService.validateProjectAuth(projects, certificate.getProjectId());
+
+        return toDTO(certificate);
+    }
+
+    /* 인증서 목록 조회 */
+    public PageResp<CertificateDTO> getCertificates(String projectId) {
+        List<CertificateDTO> list = certificateRepository
+                .findByProjectIdAndIsDeleted(projectId, false)
+                .stream()
+                .map(this::toDTO)
+                .toList();
+
+        return PageResp.<CertificateDTO>builder()
+                .contents(list)
+                .build();
+    }
+
+    /* 인증서 생성 */
+    public CertificateDTO createCertificate(String projectId, CertificateDTO dto, List<String> projects) {
+        // 프로젝트 권한 검증
+        authService.validateProjectAuth(projects, projectId);
+
+        // DTO 유효성 검사
+        validateDTO(dto);
+
+        Certificate certificate = Certificate.builder()
+                .projectId(projectId)
+                .domain(dto.getDomain())
+                .description(dto.getDescription())
+                .isDeleted(false)
+                .build();
+
+        certificateRepository.save(certificate);
+
+        // 생성 로직 (certbot 호출 등) 필요 시 추가
+
+        return toDTO(certificate);
+    }
+
+    /* 인증서 수정  */
+    public CertificateDTO editCertificate(Long certificateId, CertificateDTO dto, List<String> projects) {
+        Certificate certificate = certificateRepository
+                .findByCertificateIdAndIsDeleted(certificateId, false)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
+
+        // 프로젝트 권한 검증
+        authService.validateProjectAuth(projects, certificate.getProjectId());
+
+        // 필요한 필드만 수정
+        if (dto.getDomain() != null) {
+            certificate.setDomain(dto.getDomain());
+        }
+        if (dto.getDescription() != null) {
+            certificate.setDescription(dto.getDescription());
+        }
+        // 기타 수정할 필드가 있다면 추가
+
+        // DB 저장
+        certificateRepository.save(certificate);
+
+        // 수정 로직(certbot 재발급 등) 필요 시 추가
+
+        return toDTO(certificate);
+    }
+
+    /* 인증서 삭제 (soft delete) */
+    public void deleteCertificate(Long certificateId, List<String> projects) {
+        Certificate certificate = certificateRepository
+                .findByCertificateIdAndIsDeleted(certificateId, false)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
+
+        // 권한 검증
+        authService.validateProjectAuth(projects, certificate.getProjectId());
+
+        // soft delete
+        certificate.setIsDeleted(true);
+        certificateRepository.save(certificate);
+
+        // (추가) 파일 제거 / certbot revoke 등 로직 필요 시
+    }
+
+    /* DTO 유효성 검사 */
+    private void validateDTO(CertificateDTO dto) {
+        for (ConstraintViolation<CertificateDTO> violation
+                : Validation.buildDefaultValidatorFactory().getValidator().validate(dto)) {
+            throw new CustomException(ErrorCode.INVALID_CONF_INPUT, violation.getMessage());
+        }
+    }
+
+    /* Entity -> DTO 변환 */
+    private CertificateDTO toDTO(Certificate certificate) {
+        return CertificateDTO.builder()
+                .certificateId(certificate.getCertificateId())
+                .projectId(certificate.getProjectId())
+                .domain(certificate.getDomain())
+                .description(certificate.getDescription())
+                .isDeleted(certificate.getIsDeleted())
+                .expiredAt(certificate.getExpiredAt())
+                .build();
+    }
+}
-- 
GitLab


From fee78971e94dc1854499e6d1fed2a2dc5f456768 Mon Sep 17 00:00:00 2001
From: NaHyun22 <nhle0217@ajou.ac.kr>
Date: Wed, 30 Apr 2025 19:10:55 +0900
Subject: [PATCH 36/41] =?UTF-8?q?feat:=20certificate=20=EC=9D=B8=EC=A6=9D?=
 =?UTF-8?q?=EC=84=9C=20=EB=B0=9C=EA=B8=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../certificate/CertificateController.java    |  2 +
 .../itda/dto/certificate/CertificateDTO.java  | 18 ++--
 .../itda/entity/certificate/Certificate.java  |  5 +-
 .../certificate/CertificateService.java       | 88 ++++++++++++++++++-
 4 files changed, 100 insertions(+), 13 deletions(-)

diff --git a/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java
index b6384c9..cf5e160 100644
--- a/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java
+++ b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java
@@ -25,6 +25,7 @@ public class CertificateController {
     public ResponseEntity<Object> create(@RequestParam String projectId,
                                          @RequestBody CertificateDTO dto,
                                          HttpServletRequest request) {
+        System.out.println("1");
         certificateService.createCertificate(
                 projectId,
                 dto,
@@ -89,5 +90,6 @@ public class CertificateController {
         );
         return ResponseEntity.ok().build();
     }
+    /*인증서 검색*/
 
 }
diff --git a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
index 35aad94..e37b75e 100644
--- a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
+++ b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
@@ -1,5 +1,6 @@
 package com.aolda.itda.dto.certificate;
 
+import com.aolda.itda.entity.certificate.Challenge;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import lombok.*;
 
@@ -12,10 +13,15 @@ import java.time.LocalDateTime;
 @JsonInclude(JsonInclude.Include.NON_NULL)
 public class CertificateDTO {
 
-    private Long certificateId;
-    private String projectId;
-    private String domain;
-    private LocalDateTime expiredAt;   // 필요 시
-    private Boolean isDeleted;
-    private String description;        // 추가 설명
+    private Long certificateId;          // 인증서 고유 ID
+    private String projectId;            // 프로젝트 식별자
+    private String domain;               // SSL 인증받을 도메인 주소
+    private String email;                // 도메인 소유자의 이메일
+    private LocalDateTime expiredAt;     // 인증서 만료일
+    private Challenge challenge;         // 챌린지 방식 (HTTP, DNS_CLOUDFLARE)
+    private Boolean isDeleted;           // 삭제 여부 (soft delete)
+    private String description;          // 설명 (필요시 자유롭게 작성)
 }
+/* 이메일, 챌린지 방식, http인지 dns인지... "*/
+//도메인, 소유자 이메일, 챌린지 방식 확실하게 들어가야함!!
+/*erd 보고 만들기*/
diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
index 469ea6a..489138f 100644
--- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
+++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
@@ -23,10 +23,6 @@ public class Certificate extends BaseTimeEntity {
     @Column(nullable = false)
     private Long certificateId;
 
-    @ManyToOne
-    @JoinColumn(nullable = false, name = "user_id")
-    private User user;
-
     @Column(length = 64)
     private String projectId;
 
@@ -57,5 +53,6 @@ public class Certificate extends BaseTimeEntity {
     }
 
     public void setDescription(String description) {
+
     }
 }
diff --git a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
index 5c0dbaf..f6f83bb 100644
--- a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
+++ b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
@@ -3,6 +3,7 @@ package com.aolda.itda.service.certificate;
 import com.aolda.itda.dto.PageResp;
 import com.aolda.itda.dto.certificate.CertificateDTO;
 import com.aolda.itda.entity.certificate.Certificate;
+import com.aolda.itda.entity.certificate.Challenge;
 import com.aolda.itda.exception.CustomException;
 import com.aolda.itda.exception.ErrorCode;
 import com.aolda.itda.repository.certificate.CertificateRepository;
@@ -14,6 +15,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.util.ArrayList;
 import java.util.List;
 
 @Service
@@ -52,10 +54,12 @@ public class CertificateService {
     }
 
     /* 인증서 생성 */
-    public CertificateDTO createCertificate(String projectId, CertificateDTO dto, List<String> projects) {
+    /*public CertificateDTO createCertificate(String projectId,
+                                            CertificateDTO dto,
+                                            List<String> projects) {
         // 프로젝트 권한 검증
         authService.validateProjectAuth(projects, projectId);
-
+        System.out.println("2");
         // DTO 유효성 검사
         validateDTO(dto);
 
@@ -67,9 +71,52 @@ public class CertificateService {
                 .build();
 
         certificateRepository.save(certificate);
-
+        System.out.println("3");
         // 생성 로직 (certbot 호출 등) 필요 시 추가
 
+        return toDTO(certificate);
+    }*/
+    /* 인증서 생성 + lego 호출 */
+    public CertificateDTO createCertificate(String projectId, CertificateDTO dto, List<String> projects) {
+
+        // 1) 권한 체크
+        authService.validateProjectAuth(projects, projectId);
+
+        // 2) DTO 검증
+        validateDTO(dto);
+
+        // 3) lego 명령어 구성
+        ProcessBuilder pb = buildLegoProcess(dto);
+
+        // 4) lego 실행
+        int exitCode;
+        try {
+            Process process = pb.start();
+            exitCode = process.waitFor();
+            if (exitCode != 0) {
+                String err = new String(process.getErrorStream().readAllBytes());
+                log.error("[lego-error] {}", err);
+                throw new CustomException(ErrorCode.FAIL_CREATE_CONF,
+                        "lego 오류: " + err);
+            }
+        } catch (Exception e) {
+            log.error("[lego-exec] {}", e.getMessage());
+            throw new CustomException(ErrorCode.FAIL_CREATE_CONF,
+                    "lego 실행 실패");
+        }
+
+        // 5) 엔티티 저장
+        Certificate certificate = Certificate.builder()
+                .projectId(projectId)
+                .domain(dto.getDomain())
+                .email(dto.getEmail())
+                .challenge(dto.getChallenge())
+                .expiredAt(dto.getExpiredAt())   // 필요 시 lego 출력 파싱
+                .isDeleted(false)
+                .description(dto.getDescription())
+                .build();
+
+        certificateRepository.save(certificate);
         return toDTO(certificate);
     }
 
@@ -134,4 +181,39 @@ public class CertificateService {
                 .expiredAt(certificate.getExpiredAt())
                 .build();
     }
+
+
+
+    /* lego ProcessBuilder 생성 */
+    private ProcessBuilder buildLegoProcess(CertificateDTO dto) {
+
+        String basePath = "/data/lego"; // 인증서 저장 루트(볼륨)
+        List<String> cmd = new ArrayList<>();
+        cmd.add("/usr/local/bin/lego");
+        cmd.add("--accept-tos");
+        cmd.add("--email");        cmd.add(dto.getEmail());
+        cmd.add("--path");         cmd.add(basePath);
+
+        if (dto.getChallenge() == Challenge.HTTP) {
+            cmd.add("--http");
+            cmd.add("--http.webroot");
+            cmd.add("/data/letsencrypt-acme-challenge");
+        } else if (dto.getChallenge() == Challenge.DNS_CLOUDFLARE) {
+            cmd.add("--dns");
+            cmd.add("cloudflare");
+            // CLOUDFLARE_API_TOKEN 환경변수를 컨테이너에 세팅했다고 가정
+        }
+
+        cmd.add("--domains");      cmd.add(dto.getDomain());
+        cmd.add("run");            // 최초 발급(run) / renew(갱신)
+
+        return new ProcessBuilder(cmd)
+                .redirectErrorStream(true);
+    }
 }
+
+
+
+// 여기서 매소드를 create로 해서 lego --accept-tos --email "email@example.com" --http --http.webroot data/letsencrypt-acme-challenge --path /data/lego --domains www.example.com run
+// 이거 이메일 . 도메인 으로 넣어서 실제 인증서 연동하기!!!
+// 그리고 Dto에 관리자 이메일, 인증서 완료일, 챌린지 방식 추가하기
\ No newline at end of file
-- 
GitLab


From 0b97807cf4cfa16a5e971d01efb26f41401e49de Mon Sep 17 00:00:00 2001
From: NaHyun22 <nhle0217@ajou.ac.kr>
Date: Wed, 7 May 2025 05:57:24 +0900
Subject: [PATCH 37/41] feat: lego

---
 .../certificate/CertificateController.java    |  47 ++--
 .../itda/dto/certificate/CertificateDTO.java  |   7 +-
 .../itda/entity/certificate/Certificate.java  |  18 +-
 .../certificate/CertificateRepository.java    |  11 +
 .../certificate/CertificateService.java       | 262 +++++++++---------
 5 files changed, 189 insertions(+), 156 deletions(-)

diff --git a/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java
index cf5e160..d08968b 100644
--- a/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java
+++ b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java
@@ -1,5 +1,6 @@
 package com.aolda.itda.controller.certificate;
 
+import com.aolda.itda.dto.PageResp;
 import com.aolda.itda.dto.certificate.CertificateDTO;
 import com.aolda.itda.service.certificate.CertificateService;
 import jakarta.servlet.http.HttpServletRequest;
@@ -19,17 +20,17 @@ public class CertificateController {
     /**
      * 인증서 생성
      * POST /api/certificate?projectId=xxx
-     * Body: CertificateDTO
      */
     @PostMapping("/certificate")
-    public ResponseEntity<Object> create(@RequestParam String projectId,
-                                         @RequestBody CertificateDTO dto,
-                                         HttpServletRequest request) {
-        System.out.println("1");
+    public ResponseEntity<Void> create(
+            @RequestParam String projectId,
+            @RequestBody CertificateDTO dto,
+            HttpServletRequest request
+    ) {
         certificateService.createCertificate(
                 projectId,
                 dto,
-                (List<String>) request.getAttribute("projects") // Forwarding과 동일하게
+                (List<String>) request.getAttribute("projects")
         );
         return ResponseEntity.ok().build();
     }
@@ -39,8 +40,10 @@ public class CertificateController {
      * GET /api/certificate?certificateId=xxx
      */
     @GetMapping("/certificate")
-    public ResponseEntity<Object> view(@RequestParam Long certificateId,
-                                       HttpServletRequest request) {
+    public ResponseEntity<CertificateDTO> view(
+            @RequestParam Long certificateId,
+            HttpServletRequest request
+    ) {
         return ResponseEntity.ok(
                 certificateService.getCertificate(
                         certificateId,
@@ -50,25 +53,29 @@ public class CertificateController {
     }
 
     /**
-     * 인증서 목록 조회
-     * GET /api/certificates?projectId=xxx
+     * 인증서 목록 조회 (domain 필터링 optional)
+     * GET /api/certificates?projectId=xxx&domain=foo
      */
     @GetMapping("/certificates")
-    public ResponseEntity<Object> lists(@RequestParam String projectId) {
+    public ResponseEntity<PageResp<CertificateDTO>> lists(
+            @RequestParam String projectId,
+            @RequestParam(required = false) String domain
+    ) {
         return ResponseEntity.ok(
-                certificateService.getCertificates(projectId)
+                certificateService.getCertificates(projectId, domain)
         );
     }
 
     /**
      * 인증서 수정
      * PATCH /api/certificate?certificateId=xxx
-     * Body: CertificateDTO
      */
     @PatchMapping("/certificate")
-    public ResponseEntity<Object> edit(@RequestParam Long certificateId,
-                                       @RequestBody CertificateDTO dto,
-                                       HttpServletRequest request) {
+    public ResponseEntity<Void> edit(
+            @RequestParam Long certificateId,
+            @RequestBody CertificateDTO dto,
+            HttpServletRequest request
+    ) {
         certificateService.editCertificate(
                 certificateId,
                 dto,
@@ -82,14 +89,14 @@ public class CertificateController {
      * DELETE /api/certificate?certificateId=xxx
      */
     @DeleteMapping("/certificate")
-    public ResponseEntity<Object> delete(@RequestParam Long certificateId,
-                                         HttpServletRequest request) {
+    public ResponseEntity<Void> delete(
+            @RequestParam Long certificateId,
+            HttpServletRequest request
+    ) {
         certificateService.deleteCertificate(
                 certificateId,
                 (List<String>) request.getAttribute("projects")
         );
         return ResponseEntity.ok().build();
     }
-    /*인증서 검색*/
-
 }
diff --git a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
index e37b75e..dcdefcf 100644
--- a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
+++ b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
@@ -14,14 +14,17 @@ import java.time.LocalDateTime;
 public class CertificateDTO {
 
     private Long certificateId;          // 인증서 고유 ID
-    private String projectId;            // 프로젝트 식별자
+//    private String projectId;            // 프로젝트 식별자
     private String domain;               // SSL 인증받을 도메인 주소
     private String email;                // 도메인 소유자의 이메일
     private LocalDateTime expiredAt;     // 인증서 만료일
+    private LocalDateTime createdAt;    // 인증서 생성일
+    private LocalDateTime updatedAt;    // 인증서 업데이트일
     private Challenge challenge;         // 챌린지 방식 (HTTP, DNS_CLOUDFLARE)
     private Boolean isDeleted;           // 삭제 여부 (soft delete)
-    private String description;          // 설명 (필요시 자유롭게 작성)
+    private String apiToken;
 }
 /* 이메일, 챌린지 방식, http인지 dns인지... "*/
 //도메인, 소유자 이메일, 챌린지 방식 확실하게 들어가야함!!
 /*erd 보고 만들기*/
+//Challenge 키는 따로 (private으로 api 키 받기)
\ No newline at end of file
diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
index 489138f..6f52656 100644
--- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
+++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
@@ -32,15 +32,15 @@ public class Certificate extends BaseTimeEntity {
     @Column(length = 64)
     private String email;
 
-    private LocalDateTime expiredAt;
+    private LocalDateTime expiredAt; //인증서 만료일
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
 
     @Enumerated(EnumType.STRING)
     private Challenge challenge;
 
     private Boolean isDeleted;
 
-    @Column(length = 256)
-    private String description;
 
     public String formatDomain() {
         return domain == null ? null : domain.replace("*", "_");
@@ -52,7 +52,17 @@ public class Certificate extends BaseTimeEntity {
     public void setDomain(String domain) {
     }
 
-    public void setDescription(String description) {
+    //    public void setDescription(String description) {
+//
+//    }
+    @Transient
+    private String apiToken;
 
+    public void setExpiredAt(LocalDateTime localDateTime) {
+
+    }
+
+    public void setEmail(String email) {
     }
 }
+
diff --git a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java
index 1ce5c1f..b8cc148 100644
--- a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java
+++ b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java
@@ -4,6 +4,7 @@ import com.aolda.itda.entity.certificate.Certificate;
 import com.aolda.itda.entity.forwarding.Forwarding;
 import org.springframework.data.jpa.repository.JpaRepository;
 
+import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Optional;
 
@@ -15,4 +16,14 @@ public interface CertificateRepository extends JpaRepository<Certificate, Long>
     // 프로젝트별 목록 조회 (Soft Delete 고려)
     List<Certificate> findByProjectIdAndIsDeleted(String projectId, Boolean isDeleted);
 
+    // 만료일이 주어진 날짜 이전인(=만료 30일 이내) 인증서 조회
+    //List<Certificate> findByExpiredAtBeforeAndIsDeleted(LocalDateTime date, Boolean isDeleted);
+
+    // 1) domain 필터링용 메서드
+    List<Certificate> findByProjectIdAndDomainContainingIgnoreCaseAndIsDeleted(
+            String projectId, String domain, Boolean isDeleted);
+
+    // 3) 만료 30일 이내 대상 조회
+    List<Certificate> findByExpiredAtBeforeAndIsDeleted(
+            LocalDateTime date, Boolean isDeleted);
 }
diff --git a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
index f6f83bb..92d4c83 100644
--- a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
+++ b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
@@ -12,9 +12,14 @@ import jakarta.validation.ConstraintViolation;
 import jakarta.validation.Validation;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -27,193 +32,190 @@ public class CertificateService {
     private final CertificateRepository certificateRepository;
     private final AuthService authService;
 
-    /* 인증서 하나 조회 */
+    /** 1) 단건 조회 **/
     public CertificateDTO getCertificate(Long certificateId, List<String> projects) {
-        Certificate certificate = certificateRepository
+        Certificate cert = certificateRepository
                 .findByCertificateIdAndIsDeleted(certificateId, false)
                 .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
-        // 여기서는 ErrorCode.NOT_FOUND_CERTIFICATE 등 새로운 코드로 교체 가능
-
-        // 프로젝트 권한 검증
-        authService.validateProjectAuth(projects, certificate.getProjectId());
-
-        return toDTO(certificate);
+        authService.validateProjectAuth(projects, cert.getProjectId());
+        return toDTO(cert);
     }
 
-    /* 인증서 목록 조회 */
-    public PageResp<CertificateDTO> getCertificates(String projectId) {
-        List<CertificateDTO> list = certificateRepository
-                .findByProjectIdAndIsDeleted(projectId, false)
-                .stream()
+    /** 1) 목록 조회 (domain 필터 optional) **/
+    public PageResp<CertificateDTO> getCertificates(String projectId, String domain) {
+        List<Certificate> list;
+        if (domain != null && !domain.isBlank()) {
+            list = certificateRepository
+                    .findByProjectIdAndDomainContainingIgnoreCaseAndIsDeleted(
+                            projectId, domain, false);
+        } else {
+            list = certificateRepository
+                    .findByProjectIdAndIsDeleted(projectId, false);
+        }
+        List<CertificateDTO> dtos = list.stream()
                 .map(this::toDTO)
                 .toList();
-
         return PageResp.<CertificateDTO>builder()
-                .contents(list)
+                .contents(dtos)
                 .build();
     }
 
-    /* 인증서 생성 */
-    /*public CertificateDTO createCertificate(String projectId,
+    /** 2) 생성: expiredAt 자동 90일 설정 + 로깅 **/
+    public CertificateDTO createCertificate(String projectId,
                                             CertificateDTO dto,
                                             List<String> projects) {
-        // 프로젝트 권한 검증
-        authService.validateProjectAuth(projects, projectId);
-        System.out.println("2");
-        // DTO 유효성 검사
-        validateDTO(dto);
-
-        Certificate certificate = Certificate.builder()
-                .projectId(projectId)
-                .domain(dto.getDomain())
-                .description(dto.getDescription())
-                .isDeleted(false)
-                .build();
-
-        certificateRepository.save(certificate);
-        System.out.println("3");
-        // 생성 로직 (certbot 호출 등) 필요 시 추가
-
-        return toDTO(certificate);
-    }*/
-    /* 인증서 생성 + lego 호출 */
-    public CertificateDTO createCertificate(String projectId, CertificateDTO dto, List<String> projects) {
-
-        // 1) 권한 체크
+        log.info("createCertificate start (project={})", projectId);
         authService.validateProjectAuth(projects, projectId);
-
-        // 2) DTO 검증
         validateDTO(dto);
 
-        // 3) lego 명령어 구성
-        ProcessBuilder pb = buildLegoProcess(dto);
+        // 발급
+        executeLego(dto);
+        log.info("certificate issued for domain={}", dto.getDomain());
 
-        // 4) lego 실행
-        int exitCode;
-        try {
-            Process process = pb.start();
-            exitCode = process.waitFor();
-            if (exitCode != 0) {
-                String err = new String(process.getErrorStream().readAllBytes());
-                log.error("[lego-error] {}", err);
-                throw new CustomException(ErrorCode.FAIL_CREATE_CONF,
-                        "lego 오류: " + err);
-            }
-        } catch (Exception e) {
-            log.error("[lego-exec] {}", e.getMessage());
-            throw new CustomException(ErrorCode.FAIL_CREATE_CONF,
-                    "lego 실행 실패");
-        }
-
-        // 5) 엔티티 저장
-        Certificate certificate = Certificate.builder()
+        // 엔티티 저장 (expiredAt 기본 90일 뒤)
+        Certificate cert = Certificate.builder()
                 .projectId(projectId)
                 .domain(dto.getDomain())
                 .email(dto.getEmail())
                 .challenge(dto.getChallenge())
-                .expiredAt(dto.getExpiredAt())   // 필요 시 lego 출력 파싱
+                .expiredAt(LocalDateTime.now().plusDays(90))
                 .isDeleted(false)
-                .description(dto.getDescription())
+                .apiToken(dto.getApiToken())
                 .build();
+        certificateRepository.save(cert);
+        log.info("certificate saved (id={}, domain={})",
+                cert.getCertificateId(), cert.getDomain());
 
-        certificateRepository.save(certificate);
-        return toDTO(certificate);
+        return toDTO(cert);
     }
 
-    /* 인증서 수정  */
-    public CertificateDTO editCertificate(Long certificateId, CertificateDTO dto, List<String> projects) {
-        Certificate certificate = certificateRepository
+    /** 4) 수정 **/
+    public CertificateDTO editCertificate(Long certificateId,
+                                          CertificateDTO dto,
+                                          List<String> projects) {
+        Certificate cert = certificateRepository
                 .findByCertificateIdAndIsDeleted(certificateId, false)
                 .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
+        authService.validateProjectAuth(projects, cert.getProjectId());
 
-        // 프로젝트 권한 검증
-        authService.validateProjectAuth(projects, certificate.getProjectId());
-
-        // 필요한 필드만 수정
-        if (dto.getDomain() != null) {
-            certificate.setDomain(dto.getDomain());
-        }
-        if (dto.getDescription() != null) {
-            certificate.setDescription(dto.getDescription());
-        }
-        // 기타 수정할 필드가 있다면 추가
-
-        // DB 저장
-        certificateRepository.save(certificate);
+        if (dto.getDomain() != null)      cert.setDomain(dto.getDomain());
+        if (dto.getEmail() != null)       cert.setEmail(dto.getEmail());
 
-        // 수정 로직(certbot 재발급 등) 필요 시 추가
-
-        return toDTO(certificate);
+        certificateRepository.save(cert);
+        log.info("certificate edited (id={}, domain={})",
+                cert.getCertificateId(), cert.getDomain());
+        return toDTO(cert);
     }
 
-    /* 인증서 삭제 (soft delete) */
+    /** 4) 삭제 + 로깅 **/
     public void deleteCertificate(Long certificateId, List<String> projects) {
-        Certificate certificate = certificateRepository
+        Certificate cert = certificateRepository
                 .findByCertificateIdAndIsDeleted(certificateId, false)
                 .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
+        authService.validateProjectAuth(projects, cert.getProjectId());
 
-        // 권한 검증
-        authService.validateProjectAuth(projects, certificate.getProjectId());
-
-        // soft delete
-        certificate.setIsDeleted(true);
-        certificateRepository.save(certificate);
+        cert.setIsDeleted(true);
+        certificateRepository.save(cert);
+        log.info("certificate deleted (id={}, domain={})",
+                cert.getCertificateId(), cert.getDomain());
+    }
 
-        // (추가) 파일 제거 / certbot revoke 등 로직 필요 시
+    /** 3) 만료 30일 전 자동 갱신 배치 **/
+    @Scheduled(cron = "0 0 3 * * *", zone = "Asia/Seoul")
+    public void renewExpiringCertificates() {
+        LocalDateTime threshold = LocalDateTime.now().plusDays(30);
+        List<Certificate> expiring = certificateRepository
+                .findByExpiredAtBeforeAndIsDeleted(threshold, false);
+
+        for (Certificate cert : expiring) {
+            try {
+                log.info("renewing (id={}, domain={})", cert.getCertificateId(), cert.getDomain());
+                CertificateDTO dto = CertificateDTO.builder()
+                        .domain(cert.getDomain())
+                        .email(cert.getEmail())
+                        .challenge(cert.getChallenge())
+                        .apiToken(cert.getApiToken())
+                        .build();
+                executeLego(dto);
+
+                cert.setExpiredAt(LocalDateTime.now().plusDays(90));
+                certificateRepository.save(cert);
+                log.info("renewed (id={}, newExpiry={})",
+                        cert.getCertificateId(), cert.getExpiredAt());
+            } catch (Exception e) {
+                log.error("failed to renew (id={}, domain={}): {}",
+                        cert.getCertificateId(), cert.getDomain(), e.getMessage());
+            }
+        }
     }
 
-    /* DTO 유효성 검사 */
+    /** DTO 유효성 검사 **/
     private void validateDTO(CertificateDTO dto) {
-        for (ConstraintViolation<CertificateDTO> violation
-                : Validation.buildDefaultValidatorFactory().getValidator().validate(dto)) {
-            throw new CustomException(ErrorCode.INVALID_CONF_INPUT, violation.getMessage());
+        for (ConstraintViolation<CertificateDTO> v :
+                Validation.buildDefaultValidatorFactory()
+                        .getValidator().validate(dto)) {
+            throw new CustomException(ErrorCode.INVALID_CONF_INPUT, v.getMessage());
         }
     }
 
-    /* Entity -> DTO 변환 */
-    private CertificateDTO toDTO(Certificate certificate) {
+    /** Entity→DTO 변환 **/
+    private CertificateDTO toDTO(Certificate cert) {
         return CertificateDTO.builder()
-                .certificateId(certificate.getCertificateId())
-                .projectId(certificate.getProjectId())
-                .domain(certificate.getDomain())
-                .description(certificate.getDescription())
-                .isDeleted(certificate.getIsDeleted())
-                .expiredAt(certificate.getExpiredAt())
+                .certificateId(cert.getCertificateId())
+
+                .domain(cert.getDomain())
+                .email(cert.getEmail())
+                .challenge(cert.getChallenge())
+                .expiredAt(cert.getExpiredAt())
+                .createdAt(cert.getCreatedAt())
+                .updatedAt(cert.getUpdatedAt())
+                .isDeleted(cert.getIsDeleted())
+                .apiToken(cert.getApiToken())
                 .build();
     }
 
+    /** 인증서 발급용 lego 실행 **/
+    private void executeLego(CertificateDTO dto) {
+        if (dto.getChallenge() == Challenge.DNS_CLOUDFLARE && dto.getApiToken() == null) {
+            throw new CustomException(ErrorCode.INVALID_CONF_INPUT,
+                    "DNS_CLOUDFLARE 챌린지는 apiToken 필요");
+        }
 
-
-    /* lego ProcessBuilder 생성 */
-    private ProcessBuilder buildLegoProcess(CertificateDTO dto) {
-
-        String basePath = "/data/lego"; // 인증서 저장 루트(볼륨)
         List<String> cmd = new ArrayList<>();
         cmd.add("/usr/local/bin/lego");
         cmd.add("--accept-tos");
-        cmd.add("--email");        cmd.add(dto.getEmail());
-        cmd.add("--path");         cmd.add(basePath);
+        cmd.add("--email");   cmd.add(dto.getEmail());
+        cmd.add("--path");    cmd.add("/data/lego");
 
         if (dto.getChallenge() == Challenge.HTTP) {
             cmd.add("--http");
-            cmd.add("--http.webroot");
-            cmd.add("/data/letsencrypt-acme-challenge");
-        } else if (dto.getChallenge() == Challenge.DNS_CLOUDFLARE) {
-            cmd.add("--dns");
-            cmd.add("cloudflare");
-            // CLOUDFLARE_API_TOKEN 환경변수를 컨테이너에 세팅했다고 가정
+            cmd.add("--http.webroot"); cmd.add("/data/letsencrypt-acme-challenge");
+        } else {
+            cmd.add("--dns");  cmd.add("cloudflare");
         }
 
-        cmd.add("--domains");      cmd.add(dto.getDomain());
-        cmd.add("run");            // 최초 발급(run) / renew(갱신)
+        cmd.add("--domains"); cmd.add(dto.getDomain());
+        cmd.add("run");
 
-        return new ProcessBuilder(cmd)
+        log.info("executing lego: {}", String.join(" ", cmd));
+        ProcessBuilder pb = new ProcessBuilder(cmd)
                 .redirectErrorStream(true);
-    }
-}
-
+        pb.environment().put("CF_DNS_API_TOKEN", dto.getApiToken());
 
+        try {
+            Process p = pb.start();
+            try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
+                r.lines().forEach(line -> log.info("[lego] {}", line));
+            }
+            if (p.waitFor() != 0) {
+                throw new CustomException(ErrorCode.FAIL_CREATE_CERT,
+                        "lego exit code=" + p.exitValue());
+            }
+        } catch (IOException | InterruptedException e) {
+            log.error("lego error", e);
+            throw new CustomException(ErrorCode.FAIL_CREATE_CERT,
+                    "lego 실행 실패: " + e.getMessage());
+        }
+    }
 
-// 여기서 매소드를 create로 해서 lego --accept-tos --email "email@example.com" --http --http.webroot data/letsencrypt-acme-challenge --path /data/lego --domains www.example.com run
-// 이거 이메일 . 도메인 으로 넣어서 실제 인증서 연동하기!!!
-// 그리고 Dto에 관리자 이메일, 인증서 완료일, 챌린지 방식 추가하기
\ No newline at end of file
+}
-- 
GitLab


From 790875fc36b0a21c37dd959a0463c8ca6b142713 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Wed, 7 May 2025 18:14:51 +0900
Subject: [PATCH 38/41] =?UTF-8?q?fix:=20conf=20=ED=8C=8C=EC=9D=BC=20?=
 =?UTF-8?q?=EC=9E=91=EC=84=B1=EC=9D=B4=20=EC=A0=9C=EB=8C=80=EB=A1=9C=20?=
 =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20?=
 =?UTF-8?q?=ED=95=B4=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/routing/RoutingController.java |  3 ++-
 .../com/aolda/itda/exception/ErrorCode.java   |  4 ++-
 .../itda/service/routing/RoutingService.java  | 25 ++++++++++++++-----
 .../aolda/itda/template/OptionTemplate.java   |  2 +-
 .../aolda/itda/template/RoutingTemplate.java  |  4 +--
 5 files changed, 27 insertions(+), 11 deletions(-)

diff --git a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
index c96e910..d15805a 100644
--- a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
+++ b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
@@ -8,6 +8,7 @@ import lombok.RequiredArgsConstructor;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
+import java.io.IOException;
 import java.util.List;
 
 @RestController
@@ -19,7 +20,7 @@ public class RoutingController {
 
     @PostMapping("/routing")
     public ResponseEntity<Object> create(@RequestParam String projectId,
-                                         @RequestBody RoutingDTO dto) {
+                                         @RequestBody RoutingDTO dto) throws IOException {
         routingService.createRouting(projectId, dto);
         return ResponseEntity.ok().build();
     }
diff --git a/src/main/java/com/aolda/itda/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java
index 3e4b0be..d75235d 100644
--- a/src/main/java/com/aolda/itda/exception/ErrorCode.java
+++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java
@@ -41,7 +41,9 @@ public enum ErrorCode {
     FAIL_NGINX_CONF_RELOAD(HttpStatus.BAD_REQUEST, "Nginx 재시작에 실패했습니다"),
 
     FAIL_DELETE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 삭제하지 못했습니다"),
-    FAIL_ROLL_BACK(HttpStatus.BAD_REQUEST, "롤백 실패");
+    FAIL_ROLL_BACK(HttpStatus.BAD_REQUEST, "롤백 실패"),
+
+    FAIL_CREATE_CERT(HttpStatus.BAD_REQUEST, "인증서 생성에 실패했습니다");
 
     private final HttpStatus status;
     private final String message;
diff --git a/src/main/java/com/aolda/itda/service/routing/RoutingService.java b/src/main/java/com/aolda/itda/service/routing/RoutingService.java
index d01eb02..90424a3 100644
--- a/src/main/java/com/aolda/itda/service/routing/RoutingService.java
+++ b/src/main/java/com/aolda/itda/service/routing/RoutingService.java
@@ -16,6 +16,7 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.HttpClientErrorException;
 import org.springframework.web.client.HttpServerErrorException;
 import org.springframework.web.client.RestTemplate;
 
@@ -79,7 +80,7 @@ public class RoutingService {
     }
 
     /* Routing 생성 */
-    public RoutingDTO createRouting(String projectId, RoutingDTO dto) {
+    public RoutingDTO createRouting(String projectId, RoutingDTO dto) throws IOException {
         /* 입력 DTO 검증 */
         validateDTO(dto);
 
@@ -129,7 +130,7 @@ public class RoutingService {
             bw.flush();
             bw.close();
         } catch (Exception e) {
-            if (file.delete()) {
+            if (!file.delete()) {
                 throw new CustomException(ErrorCode.FAIL_DELETE_CONF);
             }
             throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "포트포워딩 Conf 파일을 작성하지 못했습니다");
@@ -139,8 +140,20 @@ public class RoutingService {
         String url = "http://nginx:8081/nginx-api/test";
         try {
             restTemplate.getForEntity(url, String.class);
-        } catch (Exception e) {
-            if (file.delete()) {
+
+        } catch (HttpClientErrorException | HttpServerErrorException e) {
+            String responseBody = e.getResponseBodyAsString();
+            System.err.println("Response Body: " + responseBody);
+
+            Path filePath = Paths.get(confPath);
+            List<String> lines = Files.readAllLines(filePath);
+
+            // 파일 내용 출력
+            for (String line : lines) {
+                System.out.println(line);
+            }
+
+            if (!file.delete()) {
                 throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
             }
             throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
@@ -151,8 +164,8 @@ public class RoutingService {
         try {
             restTemplate.getForEntity(url, String.class);
         } catch (Exception e) {
-            if (file.delete()) {
-                throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
+            if (!file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD, "(롤백 실패)");
             }
             throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
         }
diff --git a/src/main/java/com/aolda/itda/template/OptionTemplate.java b/src/main/java/com/aolda/itda/template/OptionTemplate.java
index 945b666..add4dfc 100644
--- a/src/main/java/com/aolda/itda/template/OptionTemplate.java
+++ b/src/main/java/com/aolda/itda/template/OptionTemplate.java
@@ -6,7 +6,7 @@ import org.springframework.stereotype.Component;
 public class OptionTemplate {
 
     public String getSSL(String certificateDomain) {
-        return "\nconf.d/include/letsencrypt-acme-challenge.conf;\n" +
+        return "\ninclude conf.d/include/letsencrypt-acme-challenge.conf;\n" +
                 "include conf.d/include/ssl-ciphers.conf;\n" +
                 "ssl_certificate /data/lego/certificates/" + certificateDomain + ".crt;\n" +
                 "ssl_certificate_key /data/lego/certificates/" + certificateDomain + ".key;\n";
diff --git a/src/main/java/com/aolda/itda/template/RoutingTemplate.java b/src/main/java/com/aolda/itda/template/RoutingTemplate.java
index 46e6116..8a53785 100644
--- a/src/main/java/com/aolda/itda/template/RoutingTemplate.java
+++ b/src/main/java/com/aolda/itda/template/RoutingTemplate.java
@@ -19,11 +19,11 @@ public class RoutingTemplate {
                 + "listen 80;\n"
                 + "listen [::]:80;\n"
                 + (dto.getCertificateId() == -1 ? "" :
-                "listen 443 ssl;\n listen [::]:443 ssl;\n")
+                "listen 443 ssl;\nlisten [::]:443 ssl;\n")
                 + "server_name " + dto.getDomain() + ";\n"
                 + (dto.getCertificateId() == -1 ? "" :
                 optionTemplate.getSSL(certificateDomain))
-                + (dto.getCaching() ? "" :
+                + (!dto.getCaching() ? "" :
                 optionTemplate.getAssetCaching()
         )
                 + "proxy_set_header Upgrade $http_upgrade;\n"
-- 
GitLab


From f9aae944798944fb064a90f5d2e127b6aa1f1475 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Wed, 7 May 2025 19:02:39 +0900
Subject: [PATCH 39/41] =?UTF-8?q?fix:=20expiredAt=EC=97=90=EC=84=9C=20expi?=
 =?UTF-8?q?resAt=EC=9C=BC=EB=A1=9C=20=ED=95=84=EB=93=9C=EB=AA=85=20?=
 =?UTF-8?q?=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../itda/dto/certificate/CertificateDTO.java  |  4 ++--
 .../itda/entity/certificate/Certificate.java  | 23 +++++--------------
 .../certificate/CertificateService.java       | 10 ++++----
 3 files changed, 13 insertions(+), 24 deletions(-)

diff --git a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
index dcdefcf..e2350cc 100644
--- a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
+++ b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
@@ -13,11 +13,11 @@ import java.time.LocalDateTime;
 @JsonInclude(JsonInclude.Include.NON_NULL)
 public class CertificateDTO {
 
-    private Long certificateId;          // 인증서 고유 ID
+    private Long id;          // 인증서 고유 ID
 //    private String projectId;            // 프로젝트 식별자
     private String domain;               // SSL 인증받을 도메인 주소
     private String email;                // 도메인 소유자의 이메일
-    private LocalDateTime expiredAt;     // 인증서 만료일
+    private LocalDateTime expiresAt;     // 인증서 만료일
     private LocalDateTime createdAt;    // 인증서 생성일
     private LocalDateTime updatedAt;    // 인증서 업데이트일
     private Challenge challenge;         // 챌린지 방식 (HTTP, DNS_CLOUDFLARE)
diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
index 6f52656..eaab407 100644
--- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
+++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
@@ -3,10 +3,7 @@ package com.aolda.itda.entity.certificate;
 import com.aolda.itda.entity.BaseTimeEntity;
 import com.aolda.itda.entity.user.User;
 import jakarta.persistence.*;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
+import lombok.*;
 
 import java.time.LocalDateTime;
 
@@ -26,13 +23,16 @@ public class Certificate extends BaseTimeEntity {
     @Column(length = 64)
     private String projectId;
 
+    @Setter
     @Column(length = 64)
     private String domain;
 
     @Column(length = 64)
+    @Setter
     private String email;
 
-    private LocalDateTime expiredAt; //인증서 만료일
+    @Setter
+    private LocalDateTime expiresAt; //인증서 만료일
     private LocalDateTime createdAt;
     private LocalDateTime updatedAt;
 
@@ -47,22 +47,11 @@ public class Certificate extends BaseTimeEntity {
     }
 
     public void setIsDeleted(boolean b) {
+        this.isDeleted = b;
     }
 
-    public void setDomain(String domain) {
-    }
-
-    //    public void setDescription(String description) {
-//
-//    }
     @Transient
     private String apiToken;
 
-    public void setExpiredAt(LocalDateTime localDateTime) {
-
-    }
-
-    public void setEmail(String email) {
-    }
 }
 
diff --git a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
index 92d4c83..250e4d2 100644
--- a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
+++ b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
@@ -78,7 +78,7 @@ public class CertificateService {
                 .domain(dto.getDomain())
                 .email(dto.getEmail())
                 .challenge(dto.getChallenge())
-                .expiredAt(LocalDateTime.now().plusDays(90))
+                .expiresAt(LocalDateTime.now().plusDays(90))
                 .isDeleted(false)
                 .apiToken(dto.getApiToken())
                 .build();
@@ -138,10 +138,10 @@ public class CertificateService {
                         .build();
                 executeLego(dto);
 
-                cert.setExpiredAt(LocalDateTime.now().plusDays(90));
+                cert.setExpiresAt(LocalDateTime.now().plusDays(90));
                 certificateRepository.save(cert);
                 log.info("renewed (id={}, newExpiry={})",
-                        cert.getCertificateId(), cert.getExpiredAt());
+                        cert.getCertificateId(), cert.getExpiresAt());
             } catch (Exception e) {
                 log.error("failed to renew (id={}, domain={}): {}",
                         cert.getCertificateId(), cert.getDomain(), e.getMessage());
@@ -161,12 +161,12 @@ public class CertificateService {
     /** Entity→DTO 변환 **/
     private CertificateDTO toDTO(Certificate cert) {
         return CertificateDTO.builder()
-                .certificateId(cert.getCertificateId())
+                .id(cert.getCertificateId())
 
                 .domain(cert.getDomain())
                 .email(cert.getEmail())
                 .challenge(cert.getChallenge())
-                .expiredAt(cert.getExpiredAt())
+                .expiresAt(cert.getExpiresAt())
                 .createdAt(cert.getCreatedAt())
                 .updatedAt(cert.getUpdatedAt())
                 .isDeleted(cert.getIsDeleted())
-- 
GitLab


From 55d6360ccddd710fa4e5eb8126c614782fa92bcf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Sun, 11 May 2025 21:47:26 +0900
Subject: [PATCH 40/41] =?UTF-8?q?feat:=20Certificate=20=EC=82=AC=EC=9A=A9?=
 =?UTF-8?q?=EC=9E=90=20=ED=99=9C=EB=8F=99=20Log=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../itda/aspect/CertificateLogAspect.java     | 148 ++++++++++++++++++
 .../aolda/itda/aspect/RoutingLogAspect.java   |  29 ++--
 .../controller/routing/RoutingController.java |   2 +-
 .../itda/dto/certificate/CertificateDTO.java  |  11 +-
 .../itda/entity/certificate/Certificate.java  |   4 +-
 .../aolda/itda/entity/routing/Routing.java    |   1 -
 .../itda/exception/ApiExceptionHandler.java   |  11 ++
 .../com/aolda/itda/exception/ErrorCode.java   |   9 +-
 .../aolda/itda/exception/ErrorResponse.java   |   2 +
 .../certificate/CertificateRepository.java    |   5 +-
 .../certificate/CertificateService.java       |   2 +-
 .../itda/service/routing/RoutingService.java  |  27 +++-
 12 files changed, 222 insertions(+), 29 deletions(-)
 create mode 100644 src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java

diff --git a/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java b/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java
new file mode 100644
index 0000000..61998c8
--- /dev/null
+++ b/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java
@@ -0,0 +1,148 @@
+package com.aolda.itda.aspect;
+
+import com.aolda.itda.dto.certificate.CertificateDTO;
+import com.aolda.itda.entity.certificate.Certificate;
+import com.aolda.itda.entity.log.Action;
+import com.aolda.itda.entity.log.Log;
+import com.aolda.itda.entity.log.ObjectType;
+import com.aolda.itda.entity.user.User;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.repository.certificate.CertificateRepository;
+import com.aolda.itda.repository.log.LogRepository;
+import com.aolda.itda.repository.user.UserRepository;
+import jakarta.persistence.EntityManager;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.util.Map;
+import java.util.Objects;
+
+@Aspect
+@Component
+@RequiredArgsConstructor
+public class CertificateLogAspect {
+
+    private final CertificateRepository certificateRepository;
+    private final UserRepository userRepository;
+    private final LogRepository logRepository;
+    private final EntityManager entityManager;
+
+    /* Create 로깅 */
+    @AfterReturning(pointcut = "execution(* com.aolda.itda.service.certificate.*Service.*create*(..))"
+            , returning = "result")
+    public void createLogging(JoinPoint joinPoint, CertificateDTO result) {
+
+        /* 사용자 조회 */
+        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+        Map<String, String> tmp = (Map<String, String>) request.getAttribute("user");
+        User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
+                () -> new CustomException(ErrorCode.NOT_FOUND_USER)
+        );
+
+        /* 생성된 엔티티 조회 */
+        Certificate certificate = certificateRepository.findByCertificateIdAndIsDeleted(result.getId(), false).orElse(null);
+
+        /* 로그 메세지 작성 */
+        String description = "domain: " + certificate.getDomain() + "\n"
+                + "email: " + certificate.getEmail() + "\n"
+                + "challenge: " + certificate.getChallenge() + "\n"
+                + "expiresAt: " + certificate.getExpiresAt();
+
+        /* 로그 엔티티 저장 */
+        logRepository.save(Log.builder()
+                .user(user)
+                .objectType(ObjectType.CERTIFICATE)
+                .objectId(certificate.getCertificateId())
+                .action(Action.CREATE)
+                .projectId(certificate.getProjectId())
+                .description(description)
+                .build());
+    }
+
+    /* Delete 로깅 */
+    @AfterReturning(pointcut = "execution(* com.aolda.itda.service.certificate.*Service.*delete*(..))")
+    public void deleteLogging(JoinPoint joinPoint) {
+
+        /* 사용자 조회 */
+        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+        Map<String, String> tmp = (Map<String, String>) request.getAttribute("user");
+        User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
+                () -> new CustomException(ErrorCode.NOT_FOUND_USER)
+        );
+
+        /* 삭제된 엔티티 조회 */
+        Object[] args = joinPoint.getArgs();
+
+        Long id = (Long) args[0];
+        Certificate certificate = certificateRepository.findByCertificateIdAndIsDeleted(id, true).orElse(null);
+
+        /* 로그 메세지 작성 */
+        String description = "domain: " + certificate.getDomain() + "\n"
+                + "email: " + certificate.getEmail() + "\n"
+                + "challenge: " + certificate.getChallenge() + "\n"
+                + "expiresAt: " + certificate.getExpiresAt();
+
+        /* 로그 엔티티 저장 */
+        logRepository.save(Log.builder()
+                .user(user)
+                .objectType(ObjectType.CERTIFICATE)
+                .objectId(certificate.getCertificateId())
+                .action(Action.DELETE)
+                .projectId(certificate.getProjectId())
+                .description(description)
+                .build());
+    }
+
+    /* Update(edit) 로깅 */
+    @Around("execution(* com.aolda.itda.service.certificate.*Service.*edit*(..))")
+    public Object editLogging(ProceedingJoinPoint joinPoint) throws Throwable {
+
+        /* 사용자 조회 */
+        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+        Map<String, String> tmp = (Map<String, String>) request.getAttribute("user");
+        User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
+                () -> new CustomException(ErrorCode.NOT_FOUND_USER)
+        );
+
+        /* 변경 전 엔티티 조회 */
+        Object[] args = joinPoint.getArgs();
+
+        Long id = (Long) args[0];
+        Certificate old = certificateRepository.findByCertificateIdAndIsDeleted(id, false).orElse(null);
+        if (old != null) {
+            entityManager.detach(old);
+        }
+
+        /* 메소드 진행 */
+        Object result = joinPoint.proceed();
+
+        /* 변경 후 엔티티 조회 */
+        Certificate newObj = certificateRepository.findByCertificateIdAndIsDeleted(id, false).orElse(null);
+
+        /* 로그 메세지 작성 */
+        String description = "domain: " + old.getDomain() + (old.getDomain().equals(newObj.getDomain()) ? "" : " -> " + newObj.getDomain()) + "\n"
+                + "email: " + old.getEmail() + (old.getEmail().equals(newObj.getEmail()) ? "" : " -> " + newObj.getEmail()) + "\n"
+                + "challenge: " + old.getChallenge() + (old.getChallenge() == newObj.getChallenge() ? "" : " -> " + newObj.getChallenge()) + "\n"
+                + "expiresAt: " + old.getExpiresAt() + (old.getExpiresAt().equals(newObj.getExpiresAt()) ? "" : " -> " + newObj.getExpiresAt());
+
+        /* 로그 엔티티 저장 */
+        logRepository.save(Log.builder()
+                .user(user)
+                .objectType(ObjectType.CERTIFICATE)
+                .objectId(newObj.getCertificateId())
+                .action(Action.UPDATE)
+                .projectId(newObj.getProjectId())
+                .description(description)
+                .build());
+        return result;
+    }
+} 
\ No newline at end of file
diff --git a/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java
index 97f7de4..8b9d5b9 100644
--- a/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java
+++ b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java
@@ -141,19 +141,14 @@ public class RoutingLogAspect {
                 + "domain: " + old.getDomain() + (old.getDomain().equals(newObj.getDomain()) ? "" : " -> " + newObj.getDomain()) + "\n"
                 + "ip: " + old.getInstanceIp() + (old.getInstanceIp().equals(newObj.getInstanceIp()) ? "" : " -> " + newObj.getInstanceIp()) + "\n"
                 + "port: " + old.getInstancePort() + (old.getInstancePort().equals(newObj.getInstancePort()) ? "" : " -> " + newObj.getInstancePort()) + "\n";
-        if (old.getCertificate() == null) {
-            if (newObj.getCertificate() != null) {
-                description = description + "certificateId: null -> " + newObj.getCertificate().getCertificateId() + "\n";
-            }
-        }
-        else {
-            if (newObj.getCertificate() == null) {
-                description = description + "certificateId: " + old.getCertificate().getCertificateId() + " -> null\n";
-            }
-            else {
-                description = description + "certificateId: " + old.getCertificate().getCertificateId() + " -> " + newObj.getCertificate().getCertificateId() + "\n";
-            }
+
+        if (isSameCertificate(old, newObj)) {
+            description = description + "certificateId: " + (old.getCertificate() == null ? "null" : old.getCertificate().getCertificateId()) + "\n";
+        } else {
+            description = description + "certificateId: " + (old.getCertificate() == null ? "null" : old.getCertificate().getCertificateId()) +
+                    (newObj.getCertificate() == null ? "null" : " -> " + newObj.getCertificate().getCertificateId()) + "\n";
         }
+
         description = description + "caching: " + (old.getCaching() == newObj.getCaching() ? newObj.getCaching() : (" -> " + newObj.getCaching()));
 
         /* 로그 엔티티 저장 */
@@ -167,4 +162,14 @@ public class RoutingLogAspect {
                 .build());
         return result;
     }
+
+    private boolean isSameCertificate(Routing old, Routing newObj) {
+        if (old.getCertificate() == null && newObj.getCertificate() == null) {
+            return true;
+        } else if (old.getCertificate() == null || newObj.getCertificate() == null) {
+            return false;
+        } else {
+            return old.getCertificate().getCertificateId().equals(newObj.getCertificate().getCertificateId());
+        }
+    }
 }
diff --git a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
index d15805a..7ca1aa8 100644
--- a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
+++ b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
@@ -40,7 +40,7 @@ public class RoutingController {
     @PatchMapping("/routing")
     public ResponseEntity<Object> edit(@RequestParam Long routingId,
                                        @RequestBody RoutingDTO dto,
-                                       HttpServletRequest request) {
+                                       HttpServletRequest request) throws IOException {
         routingService.editRouting(routingId, dto, (List<String>) request.getAttribute("projects"));
         return ResponseEntity.ok().build();
     }
diff --git a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
index e2350cc..23b9e95 100644
--- a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
+++ b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
@@ -1,7 +1,9 @@
 package com.aolda.itda.dto.certificate;
 
 import com.aolda.itda.entity.certificate.Challenge;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonInclude;
+import jakarta.persistence.Column;
 import lombok.*;
 
 import java.time.LocalDateTime;
@@ -16,9 +18,12 @@ public class CertificateDTO {
     private Long id;          // 인증서 고유 ID
 //    private String projectId;            // 프로젝트 식별자
     private String domain;               // SSL 인증받을 도메인 주소
-    private String email;                // 도메인 소유자의 이메일
-    private LocalDateTime expiresAt;     // 인증서 만료일
-    private LocalDateTime createdAt;    // 인증서 생성일
+    private String email;
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")// 도메인 소유자의 이메일
+    private LocalDateTime expiresAt;
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")// 인증서 만료일
+    private LocalDateTime createdAt;
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")// 인증서 생성일
     private LocalDateTime updatedAt;    // 인증서 업데이트일
     private Challenge challenge;         // 챌린지 방식 (HTTP, DNS_CLOUDFLARE)
     private Boolean isDeleted;           // 삭제 여부 (soft delete)
diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
index eaab407..8ebc454 100644
--- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
+++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
@@ -2,6 +2,7 @@ package com.aolda.itda.entity.certificate;
 
 import com.aolda.itda.entity.BaseTimeEntity;
 import com.aolda.itda.entity.user.User;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import jakarta.persistence.*;
 import lombok.*;
 
@@ -32,9 +33,8 @@ public class Certificate extends BaseTimeEntity {
     private String email;
 
     @Setter
+    @Column(columnDefinition = "DATETIME")
     private LocalDateTime expiresAt; //인증서 만료일
-    private LocalDateTime createdAt;
-    private LocalDateTime updatedAt;
 
     @Enumerated(EnumType.STRING)
     private Challenge challenge;
diff --git a/src/main/java/com/aolda/itda/entity/routing/Routing.java b/src/main/java/com/aolda/itda/entity/routing/Routing.java
index 94ecb55..9412368 100644
--- a/src/main/java/com/aolda/itda/entity/routing/Routing.java
+++ b/src/main/java/com/aolda/itda/entity/routing/Routing.java
@@ -69,7 +69,6 @@ public class Routing extends BaseTimeEntity {
         this.domain = dto.getDomain() != null ? dto.getDomain() : this.domain;
         this.certificate = certificate;
     }
-
     public void delete() {
         this.isDeleted = true;
     }
diff --git a/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java b/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java
index 8dbb993..20ef9ec 100644
--- a/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java
+++ b/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java
@@ -14,4 +14,15 @@ public class ApiExceptionHandler {
         log.error("[handleCustomException] {} : {}, {}", e.getErrorCode().name(), e.getErrorCode().getMessage(), e.getStackTrace());
         return ErrorResponse.fromException(e);
     }
+
+    @ExceptionHandler(Exception.class)
+    public ResponseEntity<ErrorResponse> handleException(Exception e) {
+        log.error("Unexpected error occurred: {}", e.getMessage(), e);
+        ErrorResponse response = new ErrorResponse(
+                ErrorCode.INTERNAL_SERVER_ERROR.getStatus(),
+                ErrorCode.INTERNAL_SERVER_ERROR.getCode(),
+                ErrorCode.INTERNAL_SERVER_ERROR.getMessage()
+        );
+        return new ResponseEntity<>(response, ErrorCode.INTERNAL_SERVER_ERROR.getStatus());
+    }
 }
diff --git a/src/main/java/com/aolda/itda/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java
index d75235d..5adaa1f 100644
--- a/src/main/java/com/aolda/itda/exception/ErrorCode.java
+++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java
@@ -43,8 +43,15 @@ public enum ErrorCode {
     FAIL_DELETE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 삭제하지 못했습니다"),
     FAIL_ROLL_BACK(HttpStatus.BAD_REQUEST, "롤백 실패"),
 
-    FAIL_CREATE_CERT(HttpStatus.BAD_REQUEST, "인증서 생성에 실패했습니다");
+    FAIL_CREATE_CERT(HttpStatus.BAD_REQUEST, "인증서 생성에 실패했습니다"),
+
+    // System
+    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다");
 
     private final HttpStatus status;
     private final String message;
+
+    public String getCode() {
+        return this.name();
+    }
 }
diff --git a/src/main/java/com/aolda/itda/exception/ErrorResponse.java b/src/main/java/com/aolda/itda/exception/ErrorResponse.java
index 1bec060..aa02002 100644
--- a/src/main/java/com/aolda/itda/exception/ErrorResponse.java
+++ b/src/main/java/com/aolda/itda/exception/ErrorResponse.java
@@ -1,5 +1,6 @@
 package com.aolda.itda.exception;
 
+import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Getter;
 import org.springframework.http.HttpStatus;
@@ -7,6 +8,7 @@ import org.springframework.http.ResponseEntity;
 
 @Getter
 @Builder
+@AllArgsConstructor
 public class ErrorResponse {
 
     private final HttpStatus status;  // HTTP 상태 코드
diff --git a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java
index b8cc148..6e0996b 100644
--- a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java
+++ b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java
@@ -1,7 +1,6 @@
 package com.aolda.itda.repository.certificate;
 
 import com.aolda.itda.entity.certificate.Certificate;
-import com.aolda.itda.entity.forwarding.Forwarding;
 import org.springframework.data.jpa.repository.JpaRepository;
 
 import java.time.LocalDateTime;
@@ -17,13 +16,13 @@ public interface CertificateRepository extends JpaRepository<Certificate, Long>
     List<Certificate> findByProjectIdAndIsDeleted(String projectId, Boolean isDeleted);
 
     // 만료일이 주어진 날짜 이전인(=만료 30일 이내) 인증서 조회
-    //List<Certificate> findByExpiredAtBeforeAndIsDeleted(LocalDateTime date, Boolean isDeleted);
+    //List<Certificate> findByExpiresAtBeforeAndIsDeleted(LocalDateTime date, Boolean isDeleted);
 
     // 1) domain 필터링용 메서드
     List<Certificate> findByProjectIdAndDomainContainingIgnoreCaseAndIsDeleted(
             String projectId, String domain, Boolean isDeleted);
 
     // 3) 만료 30일 이내 대상 조회
-    List<Certificate> findByExpiredAtBeforeAndIsDeleted(
+    List<Certificate> findByExpiresAtBeforeAndIsDeleted(
             LocalDateTime date, Boolean isDeleted);
 }
diff --git a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
index 250e4d2..f8d8cfe 100644
--- a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
+++ b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
@@ -125,7 +125,7 @@ public class CertificateService {
     public void renewExpiringCertificates() {
         LocalDateTime threshold = LocalDateTime.now().plusDays(30);
         List<Certificate> expiring = certificateRepository
-                .findByExpiredAtBeforeAndIsDeleted(threshold, false);
+                .findByExpiresAtBeforeAndIsDeleted(threshold, false);
 
         for (Certificate cert : expiring) {
             try {
diff --git a/src/main/java/com/aolda/itda/service/routing/RoutingService.java b/src/main/java/com/aolda/itda/service/routing/RoutingService.java
index 90424a3..8a50622 100644
--- a/src/main/java/com/aolda/itda/service/routing/RoutingService.java
+++ b/src/main/java/com/aolda/itda/service/routing/RoutingService.java
@@ -174,7 +174,7 @@ public class RoutingService {
     }
 
     /* Routing 수정 */
-    public void editRouting(Long routingId, RoutingDTO dto, List<String> projects) {
+    public void editRouting(Long routingId, RoutingDTO dto, List<String> projects) throws IOException {
         Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false)
                 .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING));
 
@@ -187,9 +187,16 @@ public class RoutingService {
         }
 
         /* SSL 인증서 조회 */
-        Certificate certificate = (dto.getCertificateId() == null) || (dto.getCertificateId() == -1 ) ? null :
-                certificateRepository.findById(dto.getCertificateId())
-                        .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_CERTIFICATE)); // isDeleted 확인 필요
+        Certificate certificate;
+        if (dto.getCertificateId() == null) {
+            certificate = routing.getCertificate();
+        }
+        else if (dto.getCertificateId() == -1) {
+            certificate = null;
+        } else {
+            certificate = certificateRepository.findById(dto.getCertificateId())
+                    .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_CERTIFICATE)); // isDeleted 확인 필요
+        }
 
         /* 파일 수정 */
         routing.edit(dto, certificate);
@@ -220,7 +227,17 @@ public class RoutingService {
         String url = "http://nginx:8081/nginx-api/test";
         try {
             restTemplate.getForEntity(url, String.class);
-        } catch (RuntimeException e) {
+        } catch (HttpClientErrorException | HttpServerErrorException e) {
+            String responseBody = e.getResponseBodyAsString();
+            System.err.println("Response Body: " + responseBody);
+
+            Path filePath = Paths.get(confPath);
+            List<String> lines = Files.readAllLines(filePath);
+
+            // 파일 내용 출력
+            for (String line : lines) {
+                System.out.println(line);
+            }
             try {
                 Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
                         , StandardCopyOption.COPY_ATTRIBUTES);
-- 
GitLab


From 7a143ba4d32523eddd06c204e873baf86f9c75b6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr>
Date: Sun, 18 May 2025 22:32:26 +0900
Subject: [PATCH 41/41] =?UTF-8?q?feat:=20Certificate=20=EA=B2=80=EC=83=89?=
 =?UTF-8?q?=EC=97=90=20=EC=99=80=EC=9D=BC=EB=93=9C=EC=B9=B4=EB=93=9C=20?=
 =?UTF-8?q?=EC=A1=B0=EA=B1=B4,=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EA=B8=B0?=
 =?UTF-8?q?=EB=B0=98=20=EA=B2=80=EC=83=89=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../itda/aspect/CertificateLogAspect.java     | 10 ++-
 .../certificate/CertificateController.java    | 12 +++-
 .../itda/dto/certificate/CertificateDTO.java  |  2 +-
 .../itda/entity/certificate/Certificate.java  |  1 +
 .../certificate/CertificateRepository.java    |  7 +-
 .../certificate/CertificateService.java       | 72 +++++++++++++++++--
 6 files changed, 91 insertions(+), 13 deletions(-)

diff --git a/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java b/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java
index 61998c8..30df250 100644
--- a/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java
+++ b/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java
@@ -23,6 +23,7 @@ import org.springframework.stereotype.Component;
 import org.springframework.web.context.request.RequestContextHolder;
 import org.springframework.web.context.request.ServletRequestAttributes;
 
+import java.time.format.DateTimeFormatter;
 import java.util.Map;
 import java.util.Objects;
 
@@ -36,6 +37,8 @@ public class CertificateLogAspect {
     private final LogRepository logRepository;
     private final EntityManager entityManager;
 
+    private static final DateTimeFormatter LOG_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
     /* Create 로깅 */
     @AfterReturning(pointcut = "execution(* com.aolda.itda.service.certificate.*Service.*create*(..))"
             , returning = "result")
@@ -55,7 +58,7 @@ public class CertificateLogAspect {
         String description = "domain: " + certificate.getDomain() + "\n"
                 + "email: " + certificate.getEmail() + "\n"
                 + "challenge: " + certificate.getChallenge() + "\n"
-                + "expiresAt: " + certificate.getExpiresAt();
+                + "expiresAt: " + (certificate.getExpiresAt() != null ? certificate.getExpiresAt().format(LOG_DATE_FORMATTER) : "null");
 
         /* 로그 엔티티 저장 */
         logRepository.save(Log.builder()
@@ -89,7 +92,7 @@ public class CertificateLogAspect {
         String description = "domain: " + certificate.getDomain() + "\n"
                 + "email: " + certificate.getEmail() + "\n"
                 + "challenge: " + certificate.getChallenge() + "\n"
-                + "expiresAt: " + certificate.getExpiresAt();
+                + "expiresAt: " + (certificate.getExpiresAt() != null ? certificate.getExpiresAt().format(LOG_DATE_FORMATTER) : "null");
 
         /* 로그 엔티티 저장 */
         logRepository.save(Log.builder()
@@ -132,7 +135,8 @@ public class CertificateLogAspect {
         String description = "domain: " + old.getDomain() + (old.getDomain().equals(newObj.getDomain()) ? "" : " -> " + newObj.getDomain()) + "\n"
                 + "email: " + old.getEmail() + (old.getEmail().equals(newObj.getEmail()) ? "" : " -> " + newObj.getEmail()) + "\n"
                 + "challenge: " + old.getChallenge() + (old.getChallenge() == newObj.getChallenge() ? "" : " -> " + newObj.getChallenge()) + "\n"
-                + "expiresAt: " + old.getExpiresAt() + (old.getExpiresAt().equals(newObj.getExpiresAt()) ? "" : " -> " + newObj.getExpiresAt());
+                + "expiresAt: " + (old.getExpiresAt() != null ? old.getExpiresAt().format(LOG_DATE_FORMATTER) : "null")
+                + (old.getExpiresAt() != null && newObj.getExpiresAt() != null && !old.getExpiresAt().equals(newObj.getExpiresAt()) ? " -> " + newObj.getExpiresAt().format(LOG_DATE_FORMATTER) : "");
 
         /* 로그 엔티티 저장 */
         logRepository.save(Log.builder()
diff --git a/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java
index d08968b..26a0322 100644
--- a/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java
+++ b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java
@@ -59,8 +59,18 @@ public class CertificateController {
     @GetMapping("/certificates")
     public ResponseEntity<PageResp<CertificateDTO>> lists(
             @RequestParam String projectId,
-            @RequestParam(required = false) String domain
+            @RequestParam(required = false) String domain,
+            @RequestParam(required = false) String query
     ) {
+
+        if (query != null) {
+            return ResponseEntity.ok(
+                    certificateService.getCertificatesWithSearch(
+                            projectId,
+                            query
+                    )
+            );
+        }
         return ResponseEntity.ok(
                 certificateService.getCertificates(projectId, domain)
         );
diff --git a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
index 23b9e95..058fdd7 100644
--- a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
+++ b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
@@ -16,7 +16,7 @@ import java.time.LocalDateTime;
 public class CertificateDTO {
 
     private Long id;          // 인증서 고유 ID
-//    private String projectId;            // 프로젝트 식별자
+    private String projectId;            // 프로젝트 식별자
     private String domain;               // SSL 인증받을 도메인 주소
     private String email;
     @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")// 도메인 소유자의 이메일
diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
index 8ebc454..9161b45 100644
--- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
+++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
@@ -34,6 +34,7 @@ public class Certificate extends BaseTimeEntity {
 
     @Setter
     @Column(columnDefinition = "DATETIME")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
     private LocalDateTime expiresAt; //인증서 만료일
 
     @Enumerated(EnumType.STRING)
diff --git a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java
index 6e0996b..0f3c137 100644
--- a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java
+++ b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java
@@ -1,7 +1,9 @@
 package com.aolda.itda.repository.certificate;
 
 import com.aolda.itda.entity.certificate.Certificate;
+import com.aolda.itda.entity.routing.Routing;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
 
 import java.time.LocalDateTime;
 import java.util.List;
@@ -19,10 +21,13 @@ public interface CertificateRepository extends JpaRepository<Certificate, Long>
     //List<Certificate> findByExpiresAtBeforeAndIsDeleted(LocalDateTime date, Boolean isDeleted);
 
     // 1) domain 필터링용 메서드
-    List<Certificate> findByProjectIdAndDomainContainingIgnoreCaseAndIsDeleted(
+    List<Certificate> findByProjectIdAndDomainContainingAndIsDeleted(
             String projectId, String domain, Boolean isDeleted);
 
     // 3) 만료 30일 이내 대상 조회
     List<Certificate> findByExpiresAtBeforeAndIsDeleted(
             LocalDateTime date, Boolean isDeleted);
+
+    @Query("SELECT r FROM Routing r WHERE r.projectId = ?1 AND r.isDeleted = ?3 AND (r.domain LIKE %?2% OR r.instanceIp LIKE %?2% OR r.name LIKE %?2%)")
+    List<Certificate> findWithSearch(String projectId, String query, Boolean isDeleted);
 }
diff --git a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
index f8d8cfe..7f42670 100644
--- a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
+++ b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
@@ -2,8 +2,10 @@ package com.aolda.itda.service.certificate;
 
 import com.aolda.itda.dto.PageResp;
 import com.aolda.itda.dto.certificate.CertificateDTO;
+import com.aolda.itda.dto.routing.RoutingDTO;
 import com.aolda.itda.entity.certificate.Certificate;
 import com.aolda.itda.entity.certificate.Challenge;
+import com.aolda.itda.entity.routing.Routing;
 import com.aolda.itda.exception.CustomException;
 import com.aolda.itda.exception.ErrorCode;
 import com.aolda.itda.repository.certificate.CertificateRepository;
@@ -12,6 +14,7 @@ import jakarta.validation.ConstraintViolation;
 import jakarta.validation.Validation;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -21,7 +24,10 @@ import java.io.IOException;
 import java.io.InputStreamReader;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
 
 @Service
 @Transactional
@@ -29,6 +35,8 @@ import java.util.List;
 @Slf4j
 public class CertificateService {
 
+    @Value("${spring.server.admin-project}")
+    private String adminProject;
     private final CertificateRepository certificateRepository;
     private final AuthService authService;
 
@@ -41,18 +49,67 @@ public class CertificateService {
         return toDTO(cert);
     }
 
+    /* Certificate 목록 조회 + 검색 */
+    public PageResp<CertificateDTO> getCertificatesWithSearch(String projectId, String query) {
+
+        /* 입력 검증 */
+        if (query == null || query.isBlank()) {
+            return PageResp.<CertificateDTO>builder()
+                    .contents(certificateRepository.findByProjectIdAndIsDeleted(projectId, false)
+                            .stream()
+                            .map(this::toDTO)
+                            .toList()).build();
+        }
+
+        /* 도메인 패턴 검증 */
+        String domainPattern = "^(\\*\\.)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$";
+        if (Pattern.matches(domainPattern, query) && query.startsWith("*.")) {
+            query = query.substring(2);
+        }
+
+        return PageResp.<CertificateDTO>builder()
+                .contents(certificateRepository.findWithSearch(projectId, query, false)
+                        .stream()
+                        .map(this::toDTO)
+                        .toList()).build();
+    }
+
     /** 1) 목록 조회 (domain 필터 optional) **/
     public PageResp<CertificateDTO> getCertificates(String projectId, String domain) {
-        List<Certificate> list;
+        Set<Certificate> set = new HashSet<>();
+        // 도메인이 입력된 경우 처리
         if (domain != null && !domain.isBlank()) {
-            list = certificateRepository
-                    .findByProjectIdAndDomainContainingIgnoreCaseAndIsDeleted(
-                            projectId, domain, false);
+            // 서브도메인이 있는 경우 
+            if (domain.indexOf('.') != domain.lastIndexOf('.')) {
+                String wildcardDomain = "*." + domain.substring(domain.indexOf('.') + 1);
+                set.addAll(certificateRepository
+                        .findByProjectIdAndDomainContainingAndIsDeleted(
+                                projectId, wildcardDomain, false));
+                set.addAll(certificateRepository
+                        .findByProjectIdAndDomainContainingAndIsDeleted(
+                                projectId, domain, false));
+                set.addAll(certificateRepository
+                        .findByProjectIdAndDomainContainingAndIsDeleted(
+                                adminProject, wildcardDomain, false));
+                set.addAll(certificateRepository
+                        .findByProjectIdAndDomainContainingAndIsDeleted(
+                                adminProject, domain, false));
+            } else {
+                // 서브도메인이 없는 경우 일반 검색
+                set.addAll(certificateRepository
+                        .findByProjectIdAndDomainContainingAndIsDeleted(
+                                projectId, domain, false));
+                set.addAll(certificateRepository
+                        .findByProjectIdAndDomainContainingAndIsDeleted(
+                                adminProject, domain, false));
+            }
         } else {
-            list = certificateRepository
-                    .findByProjectIdAndIsDeleted(projectId, false);
+            set.addAll(certificateRepository
+                    .findByProjectIdAndIsDeleted(projectId, false));
+            set.addAll(certificateRepository
+                    .findByProjectIdAndIsDeleted(adminProject, false));
         }
-        List<CertificateDTO> dtos = list.stream()
+        List<CertificateDTO> dtos = set.stream()
                 .map(this::toDTO)
                 .toList();
         return PageResp.<CertificateDTO>builder()
@@ -171,6 +228,7 @@ public class CertificateService {
                 .updatedAt(cert.getUpdatedAt())
                 .isDeleted(cert.getIsDeleted())
                 .apiToken(cert.getApiToken())
+                .projectId(cert.getProjectId())
                 .build();
     }
 
-- 
GitLab