Repository: chic2chic/popi-manager-server
Pull request #8: feat: 회원가입 구현
Author: login: 0ssang
Labels: name: ✨ feature
Created: 2025-04-26T05:10:30Z | Merged: 2025-04-27T13:11:49Z

## 🌱 관련 이슈

- [LCR-41]

---
## 📌 작업 내용 및 특이사항

- spring security 의존성 추가하였습니다.
- security config 설정 (회원 가입 부분만 구현, 로그인 및 Jwt 설정 x)
- Service 레이어 테스트 코드 작성했습니다.
- 코드 리뷰받은 항목들 수정 완료했습니다.

---
## 📚 참고사항

- 컨벤션 및 디렉토리 구조 피드백 부탁드립니다!!


[LCR-41]: https://lgcns-retail.atlassian.net/browse/LCR-41?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/.gitignore b/.gitignore
index c2065bc2..a29ebd93 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,3 +35,7 @@ out/
 
 ### VS Code ###
 .vscode/
+
+### Custom ###
+.DS_Store
+**/.DS_Store
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 3ccde621..a2ea622c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -36,12 +36,19 @@ dependencies {
     // Swagger
     implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6'
 
-
     // QueryDSL
     implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
     annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
     annotationProcessor "jakarta.annotation:jakarta.annotation-api"
     annotationProcessor "jakarta.persistence:jakarta.persistence-api"
+    // Security
+    implementation 'org.springframework.boot:spring-boot-starter-security'
+    testImplementation 'org.springframework.security:spring-security-test'
+
+    // Spring Validation
+    implementation 'org.springframework.boot:spring-boot-starter-validation'
+
+    //
 }
 
 spotless {
diff --git a/src/main/java/com/lgcns/domain/manager/controller/ManagerController.java b/src/main/java/com/lgcns/domain/manager/controller/ManagerController.java
new file mode 100644
index 00000000..1ccd6ab4
--- /dev/null
+++ b/src/main/java/com/lgcns/domain/manager/controller/ManagerController.java
@@ -0,0 +1,29 @@
+package com.lgcns.domain.manager.controller;
+
+import com.lgcns.domain.manager.dto.Request.ManagerCreateRequest;
+import com.lgcns.domain.manager.service.ManagerService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+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("/auth")
+@RequiredArgsConstructor
+@Tag(name = "1. 인증 API", description = "인증 관련 API 입니다.")
+public class ManagerController {
+    private final ManagerService managerService;
+
+    @PostMapping("/signup")
+    @Operation(summary = "운영자 회원가입", description = "운영자가 직접 가입하지 않음")
+    public ResponseEntity<Void> managerCreate(@RequestBody @Valid ManagerCreateRequest request) {
+        managerService.createManager(request);
+        return ResponseEntity.status(HttpStatus.CREATED).build();
+    }
+}
diff --git a/src/main/java/com/lgcns/domain/manager/domain/Manager.java b/src/main/java/com/lgcns/domain/manager/domain/Manager.java
new file mode 100644
index 00000000..6e4a4d23
--- /dev/null
+++ b/src/main/java/com/lgcns/domain/manager/domain/Manager.java
@@ -0,0 +1,32 @@
+package com.lgcns.domain.manager.domain;
+
+import com.lgcns.global.model.BaseTimeEntity;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Manager extends BaseTimeEntity {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "manager_id")
+    private Long id;
+
+    private String username;
+
+    private String password;
+
+    @Builder(access = AccessLevel.PRIVATE)
+    private Manager(String username, String password) {
+        this.username = username;
+        this.password = password;
+    }
+
+    public static Manager createManager(String username, String password) {
+        return Manager.builder().username(username).password(password).build();
+    }
+}
diff --git a/src/main/java/com/lgcns/domain/manager/dto/Request/ManagerCreateRequest.java b/src/main/java/com/lgcns/domain/manager/dto/Request/ManagerCreateRequest.java
new file mode 100644
index 00000000..6c211057
--- /dev/null
+++ b/src/main/java/com/lgcns/domain/manager/dto/Request/ManagerCreateRequest.java
@@ -0,0 +1,16 @@
+package com.lgcns.domain.manager.dto.Request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+
+@Schema(description = "매니저 생성 요청 DTO")
+public record ManagerCreateRequest(
+        @Schema(description = "사용자 이름", example = "manager123", required = true)
+                @NotBlank(message = "username은 필수입니다.")
+                @Size(min = 4, max = 20, message = "사용자 이름은 4~20자 사이어야 합니다.")
+                String username,
+        @Schema(description = "비밀번호", example = "password123", required = true)
+                @NotBlank(message = "password는 필수입니다.")
+                @Size(min = 8, max = 20, message = "비밀번호는 8~20자 사이어야 합니다.")
+                String password) {}
diff --git a/src/main/java/com/lgcns/domain/manager/exception/ManagerErrorCode.java b/src/main/java/com/lgcns/domain/manager/exception/ManagerErrorCode.java
new file mode 100644
index 00000000..a3dff496
--- /dev/null
+++ b/src/main/java/com/lgcns/domain/manager/exception/ManagerErrorCode.java
@@ -0,0 +1,20 @@
+package com.lgcns.domain.manager.exception;
+
+import com.lgcns.global.error.exception.ErrorCode;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@AllArgsConstructor
+public enum ManagerErrorCode implements ErrorCode {
+    DUPLICATE_USERNAME(HttpStatus.CONFLICT, "이미 사용 중인 사용자 이름입니다.");
+
+    private final HttpStatus httpStatus;
+    private final String message;
+
+    @Override
+    public String getErrorName() {
+        return this.name();
+    }
+}
diff --git a/src/main/java/com/lgcns/domain/manager/repository/ManagerRepository.java b/src/main/java/com/lgcns/domain/manager/repository/ManagerRepository.java
new file mode 100644
index 00000000..49c1640f
--- /dev/null
+++ b/src/main/java/com/lgcns/domain/manager/repository/ManagerRepository.java
@@ -0,0 +1,11 @@
+package com.lgcns.domain.manager.repository;
+
+import com.lgcns.domain.manager.domain.Manager;
+import java.util.Optional;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface ManagerRepository extends JpaRepository<Manager, Long> {
+    boolean existsByUsername(String username);
+
+    Optional<Manager> findByUsername(String username);
+}
diff --git a/src/main/java/com/lgcns/domain/manager/service/ManagerService.java b/src/main/java/com/lgcns/domain/manager/service/ManagerService.java
new file mode 100644
index 00000000..bcb3715f
--- /dev/null
+++ b/src/main/java/com/lgcns/domain/manager/service/ManagerService.java
@@ -0,0 +1,7 @@
+package com.lgcns.domain.manager.service;
+
+import com.lgcns.domain.manager.dto.Request.ManagerCreateRequest;
+
+public interface ManagerService {
+    void createManager(ManagerCreateRequest request);
+}
diff --git a/src/main/java/com/lgcns/domain/manager/service/ManagerServiceImpl.java b/src/main/java/com/lgcns/domain/manager/service/ManagerServiceImpl.java
new file mode 100644
index 00000000..20abc3ba
--- /dev/null
+++ b/src/main/java/com/lgcns/domain/manager/service/ManagerServiceImpl.java
@@ -0,0 +1,33 @@
+package com.lgcns.domain.manager.service;
+
+import com.lgcns.domain.manager.domain.Manager;
+import com.lgcns.domain.manager.dto.Request.ManagerCreateRequest;
+import com.lgcns.domain.manager.exception.ManagerErrorCode;
+import com.lgcns.domain.manager.repository.ManagerRepository;
+import com.lgcns.global.error.exception.CustomException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@Transactional
+public class ManagerServiceImpl implements ManagerService {
+    private final ManagerRepository managerRepository;
+    private final PasswordEncoder passwordEncoder;
+
+    @Override
+    public void createManager(ManagerCreateRequest request) {
+
+        if (managerRepository.existsByUsername(request.username())) {
+            throw new CustomException(ManagerErrorCode.DUPLICATE_USERNAME);
+        }
+
+        String encodedPassword = passwordEncoder.encode(request.password());
+
+        Manager manager = Manager.createManager(request.username(), encodedPassword);
+
+        managerRepository.save(manager);
+    }
+}
diff --git a/src/main/java/com/lgcns/global/config/security/WebSecurityConfig.java b/src/main/java/com/lgcns/global/config/security/WebSecurityConfig.java
new file mode 100644
index 00000000..2cfcccbf
--- /dev/null
+++ b/src/main/java/com/lgcns/global/config/security/WebSecurityConfig.java
@@ -0,0 +1,67 @@
+package com.lgcns.global.config.security;
+
+import static org.springframework.http.HttpHeaders.SET_COOKIE;
+import static org.springframework.security.config.Customizer.withDefaults;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class WebSecurityConfig {
+    @Bean
+    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+        http.csrf(AbstractHttpConfigurer::disable)
+                .headers(
+                        header ->
+                                header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
+                .formLogin(AbstractHttpConfigurer::disable)
+                .cors(withDefaults())
+                .httpBasic(AbstractHttpConfigurer::disable)
+                .authorizeHttpRequests(
+                        authorize ->
+                                authorize
+                                        .requestMatchers("/auth/signup")
+                                        .permitAll()
+                                        .requestMatchers("/swagger-ui/**", "/v3/api-docs/**")
+                                        .permitAll()
+                                        .requestMatchers("/h2-console/**")
+                                        .permitAll()
+                                        .anyRequest()
+                                        .authenticated());
+
+        return http.build();
+    }
+
+    @Bean
+    public CorsConfigurationSource corsConfigurationSource() {
+        CorsConfiguration configuration = new CorsConfiguration();
+
+        configuration.addAllowedHeader("*");
+        configuration.addAllowedMethod("*");
+        configuration.setAllowCredentials(true);
+        configuration.setMaxAge(3600L);
+        configuration.addExposedHeader(SET_COOKIE);
+
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        source.registerCorsConfiguration("/**", configuration);
+        return source;
+    }
+
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index c4aec46c..90370f50 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -3,7 +3,30 @@ spring-doc:
   default-produces-media-type: application/json
   swagger-ui:
     tags-sorter: alpha
-    operations-sorter : method
+    operations-sorter: method
     disable-swagger-default-url: true
     path: /swagger-ui
-    doc-expansion : none
\ No newline at end of file
+    doc-expansion: none
+
+spring:
+  datasource:
+    url: jdbc:h2:mem:managerdb
+    username: sa
+    password:
+    driver-class-name: org.h2.Driver
+  jpa:
+    hibernate:
+      ddl-auto: update
+    properties:
+      hibernate:
+        format_sql: true
+        show_sql: true
+    open-in-view: false
+  h2:
+    console:
+      enabled: true
+      path: /h2-console
+
+logging:
+  level:
+    org.springframework.security: DEBUG
\ No newline at end of file
diff --git a/src/test/java/com/lgcns/IntegrationTest.java b/src/test/java/com/lgcns/IntegrationTest.java
new file mode 100644
index 00000000..ddbe6d91
--- /dev/null
+++ b/src/test/java/com/lgcns/IntegrationTest.java
@@ -0,0 +1,8 @@
+package com.lgcns;
+
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+@SpringBootTest
+@ActiveProfiles("test")
+public abstract class IntegrationTest {}
diff --git a/src/test/java/com/lgcns/domain/manager/service/ManagerServiceTest.java b/src/test/java/com/lgcns/domain/manager/service/ManagerServiceTest.java
new file mode 100644
index 00000000..9f7ad6c0
--- /dev/null
+++ b/src/test/java/com/lgcns/domain/manager/service/ManagerServiceTest.java
@@ -0,0 +1,70 @@
+package com.lgcns.domain.manager.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import com.lgcns.IntegrationTest;
+import com.lgcns.domain.manager.domain.Manager;
+import com.lgcns.domain.manager.dto.Request.ManagerCreateRequest;
+import com.lgcns.domain.manager.exception.ManagerErrorCode;
+import com.lgcns.domain.manager.repository.ManagerRepository;
+import com.lgcns.global.error.exception.CustomException;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.transaction.annotation.Transactional;
+
+class ManagerServiceTest extends IntegrationTest {
+    @Autowired private ManagerRepository managerRepository;
+    @Autowired private ManagerService managerService;
+    @Autowired private PasswordEncoder passwordEncoder;
+
+    @Nested
+    class 운영자_회원가입을_할_때 {
+
+        @Test
+        @Transactional
+        void 유효한_입력_값이면_회원가입에_성공한다() {
+            // given
+            String username = "testmanager";
+            String password = "password123";
+            ManagerCreateRequest request = new ManagerCreateRequest(username, password);
+
+            // when
+            managerService.createManager(request);
+
+            // then
+            Manager savedManager =
+                    managerRepository
+                            .findByUsername(username)
+                            .orElseThrow(() -> new AssertionError("Manager 조회 실패"));
+            assertThat(savedManager.getUsername()).isEqualTo(username);
+            assertThat(passwordEncoder.matches(password, savedManager.getPassword())).isTrue();
+        }
+
+        @Test
+        @Transactional
+        void 이미_존재하는_운영_이름으로_가입아면_예외가_발생한다() {
+            // given
+            String username = "existingmanager";
+            String password = "password123";
+
+            Manager existingManager = createManager(username, "existingpassword");
+            managerRepository.save(existingManager);
+
+            ManagerCreateRequest request = new ManagerCreateRequest(username, password);
+
+            // when & then
+            assertThatThrownBy(() -> managerService.createManager(request))
+                    .isInstanceOf(CustomException.class)
+                    .hasFieldOrPropertyWithValue("errorCode", ManagerErrorCode.DUPLICATE_USERNAME);
+        }
+
+        private Manager createManager(String username, String plainPassword) {
+            String encodedPassword = passwordEncoder.encode(plainPassword);
+
+            return Manager.createManager(username, encodedPassword);
+        }
+    }
+}


Reviews:
user: login: ht3064
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T05:15:16Z
user: login: ht3064
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T05:17:31Z
user: login: 0ssang
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T05:20:00Z
user: login: ht3064
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T05:24:25Z
user: login: woogieon8on
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T05:24:34Z
user: login: ohhyungsuh
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T05:25:34Z
user: login: 0ssang
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T05:26:07Z
user: login: ht3064
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T05:26:27Z
user: login: woogieon8on
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T05:27:22Z
user: login: ohhyungsuh
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T05:29:09Z
user: login: ht3064
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T05:34:26Z
user: login: ht3064
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T05:44:44Z
user: login: woogieon8on
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-26T05:45:21Z
user: login: 0ssang
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T06:29:14Z
user: login: 0ssang
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T06:34:55Z
user: login: 0ssang
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T06:38:18Z
user: login: 0ssang
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T06:38:35Z
user: login: ht3064
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T16:46:14Z
user: login: ht3064
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T16:47:35Z
user: login: ohhyungsuh
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-27T01:02:05Z
user: login: 0ssang
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-27T03:21:53Z
user: login: ht3064
body: 고생하셨습니다 👍 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-27T12:05:15Z
user: login: ohhyungsuh
body: 수정하느라 고생 많으셨습니다! LGTM ㅎㅎ
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-27T13:10:13Z

Review comments:
diff_hunk: @@ -0,0 +1,31 @@
+package com.lgcns.domain.manager.controller;
+
+import com.lgcns.domain.manager.dto.ManagerCreateRequest;
+import com.lgcns.domain.manager.dto.ManagerCreateResponse;
+import com.lgcns.domain.manager.service.ManagerService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+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("/auth")
+@RequiredArgsConstructor
+@Tag(name = "Manager", description = "운영자 API")
+public class ManagerController {
+    private final ManagerService managerService;
+
+    @PostMapping("/signup")
+    @Operation(summary = "운영자 회원가입", description = "운영자가 직접 가입하지 않음")
+    public ResponseEntity<ManagerCreateResponse> managerCreate(
path: src/main/java/com/lgcns/domain/manager/controller/ManagerController.java
user: login: ht3064
body: 반환 타입 Void로 수정해주세요!
created_at: 2025-04-26T05:15:16Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,22 @@
+package com.lgcns.domain.manager.exception;
+
+import com.lgcns.global.error.exception.ErrorCode;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@AllArgsConstructor
+public enum ManagerErrorCode implements ErrorCode {
+    DUPLICATE_USERNAME(HttpStatus.BAD_REQUEST, "이미 사용 중인 사용자 이름입니다."),
path: src/main/java/com/lgcns/domain/manager/exception/ManagerErrorCode.java
user: login: ht3064
body: HTTP 상태 코드 409가 더 적합할 듯 싶습니다.
created_at: 2025-04-26T05:17:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,22 @@
+package com.lgcns.domain.manager.exception;
+
+import com.lgcns.global.error.exception.ErrorCode;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@AllArgsConstructor
+public enum ManagerErrorCode implements ErrorCode {
+    DUPLICATE_USERNAME(HttpStatus.BAD_REQUEST, "이미 사용 중인 사용자 이름입니다."),
path: src/main/java/com/lgcns/domain/manager/exception/ManagerErrorCode.java
user: login: 0ssang
body: 확인했습니다!
created_at: 2025-04-26T05:20:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,7 @@
+package com.lgcns.domain.manager.dto;
+
+public record ManagerCreateResponse(Long id, String username) {
+    public static ManagerCreateResponse from(Long id, String username) {
path: src/main/java/com/lgcns/domain/manager/dto/ManagerCreateResponse.java
user: login: ht3064
body: 필드 값들을 바로 받아서 객체를 생성할 때 정적 팩토리 메서드는 보통 of 네이밍을 사용하니 변경바랍니다.
created_at: 2025-04-26T05:24:25Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,52 @@
+package com.lgcns.global.config.security;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class WebSecurityConfig {
+    @Bean
+    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+        http.csrf(AbstractHttpConfigurer::disable)
+                .headers(
+                        header ->
+                                header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
+                .formLogin(AbstractHttpConfigurer::disable)
+                .cors(AbstractHttpConfigurer::disable)
+                .httpBasic(AbstractHttpConfigurer::disable)
+                .headers(
+                        headers ->
+                                headers.frameOptions(
+                                        HeadersConfigurer.FrameOptionsConfig
+                                                ::sameOrigin) // 같은 출처의 프레임 로드 허용
path: src/main/java/com/lgcns/global/config/security/WebSecurityConfig.java
user: login: woogieon8on
body: 위 설정에서 FrameOptionsConfig를 disable로 설정하고, 아래 설정에서는 sameOrigin으로 설정했는데,
이렇게 설정하면 두 설정이 함께 적용되나요?
혹시 아래 설정으로 덮어쓰기 되는건지 궁금합니다
created_at: 2025-04-26T05:24:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,39 @@
+package com.lgcns.domain.manager.service;
+
+import com.lgcns.domain.manager.domain.Manager;
+import com.lgcns.domain.manager.dto.ManagerCreateRequest;
+import com.lgcns.domain.manager.dto.ManagerCreateResponse;
+import com.lgcns.domain.manager.exception.ManagerErrorCode;
+import com.lgcns.domain.manager.repository.ManagerRepository;
+import com.lgcns.global.error.exception.CustomException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
+public class ManagerCreateServiceImpl implements ManagerService {
path: src/main/java/com/lgcns/domain/manager/service/ManagerCreateServiceImpl.java
user: login: ohhyungsuh
body: 클래스 명을 ManagerServiceImpl로 하는 게 적합해보입니다!
created_at: 2025-04-26T05:25:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,52 @@
+package com.lgcns.global.config.security;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class WebSecurityConfig {
+    @Bean
+    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+        http.csrf(AbstractHttpConfigurer::disable)
+                .headers(
+                        header ->
+                                header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
+                .formLogin(AbstractHttpConfigurer::disable)
+                .cors(AbstractHttpConfigurer::disable)
+                .httpBasic(AbstractHttpConfigurer::disable)
+                .headers(
+                        headers ->
+                                headers.frameOptions(
+                                        HeadersConfigurer.FrameOptionsConfig
+                                                ::sameOrigin) // 같은 출처의 프레임 로드 허용
path: src/main/java/com/lgcns/global/config/security/WebSecurityConfig.java
user: login: 0ssang
body: 위의 설정으로 적용되는것 같습니다. 밑의 내용을 삭제하도록 하겠습니다!
created_at: 2025-04-26T05:26:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,7 @@
+package com.lgcns.domain.manager.dto;
+
+public record ManagerCreateResponse(Long id, String username) {
+    public static ManagerCreateResponse from(Long id, String username) {
path: src/main/java/com/lgcns/domain/manager/dto/ManagerCreateResponse.java
user: login: ht3064
body: request, response dto에 스키마 다 남겨주세요!
created_at: 2025-04-26T05:26:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,52 @@
+package com.lgcns.global.config.security;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class WebSecurityConfig {
+    @Bean
+    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+        http.csrf(AbstractHttpConfigurer::disable)
+                .headers(
+                        header ->
+                                header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
+                .formLogin(AbstractHttpConfigurer::disable)
+                .cors(AbstractHttpConfigurer::disable)
+                .httpBasic(AbstractHttpConfigurer::disable)
+                .headers(
+                        headers ->
+                                headers.frameOptions(
+                                        HeadersConfigurer.FrameOptionsConfig
+                                                ::sameOrigin) // 같은 출처의 프레임 로드 허용
path: src/main/java/com/lgcns/global/config/security/WebSecurityConfig.java
user: login: woogieon8on
body: 감사합니당 LGTM
created_at: 2025-04-26T05:27:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,41 @@
+package com.lgcns.domain.manager.domain;
+
+import com.lgcns.global.model.BaseTimeEntity;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@Entity
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Manager extends BaseTimeEntity {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "manager_id")
+    private Long id;
+
+    private String username;
+
+    private String password;
+
+    @Builder(access = AccessLevel.PRIVATE)
+    private Manager(String username, String password) {
+        this.username = username;
+        this.password = password;
+    }
+
+    public static Manager createManager(
+            String username, String password, PasswordEncoder passwordEncoder) {
+        return Manager.builder()
+                .username(username)
+                .password(passwordEncoder.encode(password))
+                .build();
+    }
+
+    public boolean checkPassword(String rawPassword, PasswordEncoder passwordEncoder) {
+        return passwordEncoder.matches(rawPassword, this.password);
+    }
path: src/main/java/com/lgcns/domain/manager/domain/Manager.java
user: login: ohhyungsuh
body: 이 메서드를 사용하고 있지 않은데, 혹시 쓰이는 곳이 있나요?
created_at: 2025-04-26T05:29:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,52 @@
+package com.lgcns.global.config.security;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class WebSecurityConfig {
+    @Bean
+    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+        http.csrf(AbstractHttpConfigurer::disable)
+                .headers(
+                        header ->
+                                header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
+                .formLogin(AbstractHttpConfigurer::disable)
+                .cors(AbstractHttpConfigurer::disable)
+                .httpBasic(AbstractHttpConfigurer::disable)
+                .headers(
+                        headers ->
+                                headers.frameOptions(
+                                        HeadersConfigurer.FrameOptionsConfig
+                                                ::sameOrigin) // 같은 출처의 프레임 로드 허용
+                        )
+                .authorizeHttpRequests(
+                        authorize ->
+                                authorize
+                                        .requestMatchers("/auth/signup")
+                                        .permitAll()
+                                        .requestMatchers("/swagger-ui/**", "/v3/api-docs/**")
+                                        .permitAll()
+                                        .requestMatchers("/h2-console/**")
+                                        .permitAll()
+                                        .anyRequest()
+                                        .authenticated());
+
+        return http.build();
+    }
+
path: src/main/java/com/lgcns/global/config/security/WebSecurityConfig.java
user: login: ht3064
body: ```java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    // 중략
            .cors(withDefaults())
    // 
}
```

```java
@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();

    configuration.addAllowedHeader("*");
    configuration.addAllowedMethod("*");
    configuration.setAllowCredentials(true);
    configuration.setMaxAge(3600L);
    configuration.addExposedHeader(SET_COOKIE);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}
```

다음과 같이 cors 설정해주세요.
created_at: 2025-04-26T05:34:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,39 @@
+package com.lgcns.domain.manager.service;
+
+import com.lgcns.domain.manager.domain.Manager;
+import com.lgcns.domain.manager.dto.ManagerCreateRequest;
+import com.lgcns.domain.manager.dto.ManagerCreateResponse;
+import com.lgcns.domain.manager.exception.ManagerErrorCode;
+import com.lgcns.domain.manager.repository.ManagerRepository;
+import com.lgcns.global.error.exception.CustomException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
path: src/main/java/com/lgcns/domain/manager/service/ManagerCreateServiceImpl.java
user: login: ht3064
body: 클래스 레벨에서는 아래와 같이 적용하고
```java
@Transactional
 ```
읽기 작업에는 메서드에 아래와 같이 처리해주세요!
```java
@Transactional(readOnly = true)
```
created_at: 2025-04-26T05:44:43Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,31 @@
+package com.lgcns.domain.manager.controller;
+
+import com.lgcns.domain.manager.dto.ManagerCreateRequest;
+import com.lgcns.domain.manager.dto.ManagerCreateResponse;
+import com.lgcns.domain.manager.service.ManagerService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+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("/auth")
+@RequiredArgsConstructor
+@Tag(name = "Manager", description = "운영자 API")
+public class ManagerController {
+    private final ManagerService managerService;
+
+    @PostMapping("/signup")
+    @Operation(summary = "운영자 회원가입", description = "운영자가 직접 가입하지 않음")
+    public ResponseEntity<ManagerCreateResponse> managerCreate(
path: src/main/java/com/lgcns/domain/manager/controller/ManagerController.java
user: login: 0ssang
body: 확인했습니다.
created_at: 2025-04-26T06:29:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,7 @@
+package com.lgcns.domain.manager.dto;
+
+public record ManagerCreateResponse(Long id, String username) {
+    public static ManagerCreateResponse from(Long id, String username) {
path: src/main/java/com/lgcns/domain/manager/dto/ManagerCreateResponse.java
user: login: 0ssang
body: 스키마 적용했고 response dto는 삭제했습니다!
created_at: 2025-04-26T06:34:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,39 @@
+package com.lgcns.domain.manager.service;
+
+import com.lgcns.domain.manager.domain.Manager;
+import com.lgcns.domain.manager.dto.ManagerCreateRequest;
+import com.lgcns.domain.manager.dto.ManagerCreateResponse;
+import com.lgcns.domain.manager.exception.ManagerErrorCode;
+import com.lgcns.domain.manager.repository.ManagerRepository;
+import com.lgcns.global.error.exception.CustomException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
+public class ManagerCreateServiceImpl implements ManagerService {
path: src/main/java/com/lgcns/domain/manager/service/ManagerCreateServiceImpl.java
user: login: 0ssang
body: 감사합니다!!
created_at: 2025-04-26T06:38:18Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,41 @@
+package com.lgcns.domain.manager.domain;
+
+import com.lgcns.global.model.BaseTimeEntity;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@Entity
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Manager extends BaseTimeEntity {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "manager_id")
+    private Long id;
+
+    private String username;
+
+    private String password;
+
+    @Builder(access = AccessLevel.PRIVATE)
+    private Manager(String username, String password) {
+        this.username = username;
+        this.password = password;
+    }
+
+    public static Manager createManager(
+            String username, String password, PasswordEncoder passwordEncoder) {
+        return Manager.builder()
+                .username(username)
+                .password(passwordEncoder.encode(password))
+                .build();
+    }
+
+    public boolean checkPassword(String rawPassword, PasswordEncoder passwordEncoder) {
+        return passwordEncoder.matches(rawPassword, this.password);
+    }
path: src/main/java/com/lgcns/domain/manager/domain/Manager.java
user: login: 0ssang
body: 삭제하도록 하겠습니다.
created_at: 2025-04-26T06:38:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,22 @@
+package com.lgcns.domain.manager.exception;
+
+import com.lgcns.global.error.exception.ErrorCode;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@AllArgsConstructor
+public enum ManagerErrorCode implements ErrorCode {
+    DUPLICATE_USERNAME(HttpStatus.CONFLICT, "이미 사용 중인 사용자 이름입니다."),
+    INVALID_PASSWORD_FORMAT(HttpStatus.BAD_REQUEST, "비밀번호 형식이 올바르지 않습니다."),
+    INVALID_USERNAME_FORMAT(HttpStatus.BAD_REQUEST, "사용자 이름 형식이 올바르지 않습니다.");
path: src/main/java/com/lgcns/domain/manager/exception/ManagerErrorCode.java
user: login: ht3064
body: 사용하지 않는 에러코드는 제거해주세요!
created_at: 2025-04-26T16:46:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,37 @@
+package com.lgcns.domain.manager.service;
+
+import com.lgcns.domain.manager.domain.Manager;
+import com.lgcns.domain.manager.dto.Request.ManagerCreateRequest;
+import com.lgcns.domain.manager.exception.ManagerErrorCode;
+import com.lgcns.domain.manager.repository.ManagerRepository;
+import com.lgcns.global.error.exception.CustomException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@Transactional
+public class ManagerServiceImpl implements ManagerService {
+    private final ManagerRepository managerRepository;
+    private final PasswordEncoder passwordEncoder;
+
+    @Override
+    public void createManager(ManagerCreateRequest request) {
+        // 사용자 이름 중복 체크
+        if (managerRepository.existsByUsername(request.username())) {
+            throw new CustomException(ManagerErrorCode.DUPLICATE_USERNAME);
+        }
+
+        // 비밇번호 인코딩
+        String encodedPassword = passwordEncoder.encode(request.password());
+
+        // Manager 엔티티 생성 - 빌더 패턴. 정팩은 삭제함
+        Manager manager =
+                Manager.builder().username(request.username()).password(encodedPassword).build();
path: src/main/java/com/lgcns/domain/manager/service/ManagerServiceImpl.java
user: login: ht3064
body: 저희 엔티티 생성을 정적 팩토리 메서드로 하기로 하였는데 빌더 패턴 사용하신 이유가 있을까요??
created_at: 2025-04-26T16:47:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,37 @@
+package com.lgcns.domain.manager.service;
+
+import com.lgcns.domain.manager.domain.Manager;
+import com.lgcns.domain.manager.dto.Request.ManagerCreateRequest;
+import com.lgcns.domain.manager.exception.ManagerErrorCode;
+import com.lgcns.domain.manager.repository.ManagerRepository;
+import com.lgcns.global.error.exception.CustomException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@Transactional
+public class ManagerServiceImpl implements ManagerService {
+    private final ManagerRepository managerRepository;
+    private final PasswordEncoder passwordEncoder;
+
+    @Override
+    public void createManager(ManagerCreateRequest request) {
+        // 사용자 이름 중복 체크
+        if (managerRepository.existsByUsername(request.username())) {
+            throw new CustomException(ManagerErrorCode.DUPLICATE_USERNAME);
+        }
+
+        // 비밇번호 인코딩
+        String encodedPassword = passwordEncoder.encode(request.password());
+
+        // Manager 엔티티 생성 - 빌더 패턴. 정팩은 삭제함
+        Manager manager =
+                Manager.builder().username(request.username()).password(encodedPassword).build();
path: src/main/java/com/lgcns/domain/manager/service/ManagerServiceImpl.java
user: login: ohhyungsuh
body: 저도 이 부분이 궁금합니다.
그리고 만약 나중에 빌더 패턴 쓴다면 아래처럼 쓰는게 좋을 것 같습니다!

```
Manager.builder()
    .username(~)
    .password(~)
    .build()
```
created_at: 2025-04-27T01:02:05Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,37 @@
+package com.lgcns.domain.manager.service;
+
+import com.lgcns.domain.manager.domain.Manager;
+import com.lgcns.domain.manager.dto.Request.ManagerCreateRequest;
+import com.lgcns.domain.manager.exception.ManagerErrorCode;
+import com.lgcns.domain.manager.repository.ManagerRepository;
+import com.lgcns.global.error.exception.CustomException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@Transactional
+public class ManagerServiceImpl implements ManagerService {
+    private final ManagerRepository managerRepository;
+    private final PasswordEncoder passwordEncoder;
+
+    @Override
+    public void createManager(ManagerCreateRequest request) {
+        // 사용자 이름 중복 체크
+        if (managerRepository.existsByUsername(request.username())) {
+            throw new CustomException(ManagerErrorCode.DUPLICATE_USERNAME);
+        }
+
+        // 비밇번호 인코딩
+        String encodedPassword = passwordEncoder.encode(request.password());
+
+        // Manager 엔티티 생성 - 빌더 패턴. 정팩은 삭제함
+        Manager manager =
+                Manager.builder().username(request.username()).password(encodedPassword).build();
path: src/main/java/com/lgcns/domain/manager/service/ManagerServiceImpl.java
user: login: 0ssang
body: 빌더가 더 익숙해서 그냥 사용했는데 컨벤션을 지키도록 하겠습니다.
created_at: 2025-04-27T03:21:53Z
author_association: MEMBER

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-27T12:08:07Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=chic2chic_popi-manager-server&pullRequest=8) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4 New issues](https://sonarcloud.io/project/issues?id=chic2chic_popi-manager-server&pullRequest=8&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=chic2chic_popi-manager-server&pullRequest=8&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=chic2chic_popi-manager-server&pullRequest=8&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [94.1% Coverage on New Code](https://sonarcloud.io/component_measures?id=chic2chic_popi-manager-server&pullRequest=8&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=chic2chic_popi-manager-server&pullRequest=8&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=chic2chic_popi-manager-server&pullRequest=8)


author_association: NONE