diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b0a82a4..4e77c71 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -79,6 +79,10 @@ jobs: echo "KAKAO_LOGIN_REDIRECT_URI=${{ secrets.KAKAO_LOGIN_REDIRECT_URI }}" >> .env echo "KAKAO_REST_API_KEY=${{ secrets.KAKAO_REST_API_KEY }}" >> .env + echo "KAKAO_JS_KEY=${{ secrets.KAKAO_JS_KEY }}" >> .env + echo "KAKAOPAY_SECRET_KEY_DEV=${{ secrets.KAKAOPAY_SECRET_KEY_DEV }}" >> .env + echo "NTS_SERVICE_KEY=${{ secrets.NTS_SERVICE_KEY }}" >> .env + # 3. 네트워크 생성 확인 sudo docker network inspect cuk-compasser-net >/dev/null 2>&1 || sudo docker network create cuk-compasser-net @@ -89,7 +93,7 @@ jobs: # 5. 미사용 이미지 정리 sudo docker image prune -f - # 6. 디스코드 알림 (성공 시) - SSH 액션 밖으로 분리 + # 6. 디스코드 알림 (성공 시) - name: Discord Notification - Success if: success() uses: sarisia/actions-status-discord@v1 @@ -103,7 +107,7 @@ jobs: 성공적으로 EC2에 배포되었습니다! color: 0x00ff00 - # 7. 디스코드 알림 (실패 시) - SSH 액션 밖으로 분리 + # 7. 디스코드 알림 (실패 시) - name: Discord Notification - Failure if: failure() uses: sarisia/actions-status-discord@v1 diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index 8821331..4c6a853 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -3,8 +3,6 @@ services: image: shawn9272/cuk-compasser:${IMAGE_TAG} container_name: cuk-compasser-service restart: always - ports: - - "8080:8080" env_file: .env environment: - DB_HOST=mysql-db @@ -25,6 +23,9 @@ services: - KAKAO_LOCAL_BASE_URL=https://dapi.kakao.com - KAKAO_REST_API_KEY=${KAKAO_REST_API_KEY} - JAVA_TOOL_OPTIONS=-Xms256m -Xmx256m + - KAKAO_JS_KEY=${KAKAO_JS_KEY} + - KAKAOPAY_SECRET_KEY_DEV=${KAKAOPAY_SECRET_KEY_DEV} + - NTS_SERVICE_KEY=${NTS_SERVICE_KEY} depends_on: mysql-db: condition: service_healthy @@ -48,6 +49,23 @@ services: limits: memory: 400M + nginx: + image: nginx:latest + container_name: nginx-proxy + restart: always + ports: + - "80:80" + - "443:443" # HTTPS 포트 개방 + volumes: + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf + # ✨ 핵심: EC2 호스트의 인증서 폴더를 Nginx 컨테이너 내부로 연결 (읽기 전용: ro) + - /etc/letsencrypt:/etc/letsencrypt:ro + depends_on: + - app + - grafana + networks: + - cuk-compasser-net + redis: image: redis:latest container_name: redis @@ -103,8 +121,6 @@ services: image: grafana/grafana:latest container_name: grafana restart: always - ports: - - "3000:3000" environment: - GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD} - GF_SERVER_DOMAIN=compasser.site diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 0000000..e0bf861 --- /dev/null +++ b/nginx/default.conf @@ -0,0 +1,48 @@ +server { + listen 80; + server_name compasser.site; + + # HTTP로 들어오는 모든 요청을 HTTPS로 강제 리다이렉트 + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + server_name compasser.site; + + # Certbot이 관리하는 SSL 인증서 설정 (경로 유지) + ssl_certificate /etc/letsencrypt/live/compasser.site/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/compasser.site/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + # 1. 그라파나 접속 설정 + location /grafana/ { + # 127.0.0.1 대신 도커 서비스 이름 사용 + proxy_pass http://grafana:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 웹소켓 지원 + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # 2. 기본 서비스 (스프링 부트) 접속 설정 + location / { + # 127.0.0.1 대신 도커 서비스 이름 사용 + proxy_pass http://app:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 웹소켓 지원 + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} \ No newline at end of file diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/oAuth2/controller/OAuth2Controller.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/oAuth2/controller/OAuth2Controller.java index 9c6133e..f1325d0 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/oAuth2/controller/OAuth2Controller.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/oAuth2/controller/OAuth2Controller.java @@ -1,6 +1,7 @@ package Comprehensive_Design_Project.CUK_Compasser.domain.oAuth2.controller; import Comprehensive_Design_Project.CUK_Compasser.domain.member.dto.MemberRespDTO; +import Comprehensive_Design_Project.CUK_Compasser.domain.member.entity.MemberRole; import Comprehensive_Design_Project.CUK_Compasser.domain.member.service.MemberService; import Comprehensive_Design_Project.CUK_Compasser.domain.oAuth2.dto.LoginReqDTO; import Comprehensive_Design_Project.CUK_Compasser.domain.oAuth2.dto.SignUpReqDTO; @@ -42,7 +43,7 @@ public class OAuth2Controller { private final OAuth2Service oAuth2Service; - public record AccessTokenDTO(String accessToken) {} + public record AccessTokenDTO(String accessToken, MemberRole role) {} @PostMapping("/sign-up") @Operation(summary = "일반 회원가입 API", description = "이름, 닉네임, 이메일 등을 입력받아 회원가입을 진행합니다.") @@ -84,7 +85,7 @@ public ApiResponse login( response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); - return ApiResponse.onSuccess(SuccessStatus.OK, new AccessTokenDTO(tokenResponse.accessToken())); + return ApiResponse.onSuccess(SuccessStatus.OK, new AccessTokenDTO(tokenResponse.accessToken(), tokenResponse.role())); } @PostMapping("/login-kakao") diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/oAuth2/dto/TokenRespDTO.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/oAuth2/dto/TokenRespDTO.java index 3ffe808..27f64ef 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/oAuth2/dto/TokenRespDTO.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/oAuth2/dto/TokenRespDTO.java @@ -1,9 +1,11 @@ package Comprehensive_Design_Project.CUK_Compasser.domain.oAuth2.dto; +import Comprehensive_Design_Project.CUK_Compasser.domain.member.entity.MemberRole; import lombok.Builder; @Builder public record TokenRespDTO ( String accessToken, - String refreshToken + String refreshToken, + MemberRole role ) { } diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/oAuth2/service/OAuth2Service.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/oAuth2/service/OAuth2Service.java index 3c5a88b..394644a 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/oAuth2/service/OAuth2Service.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/oAuth2/service/OAuth2Service.java @@ -113,6 +113,9 @@ public TokenRespDTO login(LoginReqDTO request) { Authentication authentication = authenticationManager.authenticate(authenticationToken); + Member member = memberRepository.findByEmail(authentication.getName()) + .orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND)); + JWT jwt = jwtProvider.generateToken(authentication); redisTemplate.opsForValue().set( @@ -125,6 +128,7 @@ public TokenRespDTO login(LoginReqDTO request) { return TokenRespDTO.builder() .accessToken(jwt.getAccessToken()) .refreshToken(jwt.getRefreshToken()) + .role(member.getRole()) .build(); } diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/service/StoreImageService.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/service/StoreImageService.java index cafa7c3..1a95e8f 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/service/StoreImageService.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/domain/store/service/StoreImageService.java @@ -7,6 +7,7 @@ import Comprehensive_Design_Project.CUK_Compasser.domain.store.repository.StoreImageRepository; import Comprehensive_Design_Project.CUK_Compasser.domain.store.repository.StoreRepository; import Comprehensive_Design_Project.CUK_Compasser.domain.storeManager.repository.StoreManagerRepository; +import Comprehensive_Design_Project.CUK_Compasser.global.aws.S3Service; import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.code.status.ErrorStatus; import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.exception.GeneralException; import lombok.RequiredArgsConstructor; @@ -14,6 +15,8 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.util.UUID; + @Service @RequiredArgsConstructor public class StoreImageService { @@ -24,6 +27,7 @@ public class StoreImageService { private final StoreImageRepository storeImageRepository; private final StoreManagerRepository storeManagerRepository; private final StoreImageConverter storeImageConverter; + private final S3Service s3Service; @Transactional(readOnly = true) public StoreImageRespDTO getRepresentativeImage(Long memberId) { @@ -42,7 +46,8 @@ public StoreImageRespDTO uploadRepresentativeImage(Long memberId, MultipartFile throw new GeneralException(ErrorStatus.STORE_IMAGE_NOT_FOUND); } - String url = "uploaded://" + storeImage.getOriginalFilename(); + String imageKey = createStoreImageKey(store.getId(), storeImage); + String url = s3Service.uploadImage(storeImage, imageKey); storeImageRepository.deleteAllByStore_Id(store.getId()); @@ -70,4 +75,17 @@ private Store getMyStoreEntity(Long memberId) { return storeRepository.findByStoreManager_MemberId(memberId) .orElseThrow(() -> new GeneralException(ErrorStatus.STORE_NOT_FOUND)); } -} \ No newline at end of file + + private String createStoreImageKey(Long storeId, MultipartFile file) { + return "stores/" + storeId + "/representative/" + UUID.randomUUID() + getExtension(file); + } + + private String getExtension(MultipartFile file) { + String originalFilename = file.getOriginalFilename(); + if (originalFilename == null || !originalFilename.contains(".")) { + return ""; + } + + return originalFilename.substring(originalFilename.lastIndexOf(".")); + } +} diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/aws/S3Service.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/aws/S3Service.java new file mode 100644 index 0000000..6f367d3 --- /dev/null +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/aws/S3Service.java @@ -0,0 +1,56 @@ +package Comprehensive_Design_Project.CUK_Compasser.global.aws; + +import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.code.status.ErrorStatus; +import Comprehensive_Design_Project.CUK_Compasser.global.common.apiPayload.exception.GeneralException; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.IOException; + +@Service +@RequiredArgsConstructor +public class S3Service { + + @Value("${spring.cloud.aws.s3.bucket}") + private String bucket; + + @Value("${spring.cloud.aws.region.static}") + private String region; + + private final S3Client s3Client; + + public String uploadImage(MultipartFile file, String key) { + validateImage(file); + + try { + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucket) + .key(key) + .contentType(file.getContentType()) + .contentLength(file.getSize()) + .build(); + + s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize())); + + return "https://" + bucket + ".s3." + region + ".amazonaws.com/" + key; + } catch (IOException e) { + throw new GeneralException(ErrorStatus.FILE_UPLOAD_FAILED); + } + } + + private void validateImage(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new GeneralException(ErrorStatus.FILE_REQUIRED); + } + + String contentType = file.getContentType(); + if (contentType == null || !contentType.startsWith("image/")) { + throw new GeneralException(ErrorStatus.UNSUPPORTED_IMAGE_TYPE); + } + } +} diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/config/SecurityConfig.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/config/SecurityConfig.java index 2f51b13..e42ea81 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/config/SecurityConfig.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/config/SecurityConfig.java @@ -17,6 +17,10 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import java.util.List; @Configuration @RequiredArgsConstructor @@ -31,6 +35,8 @@ public JWTAuthenticationFilter jwtAuthenticationFilter(JWTProvider jwtProvider, SecurityFilterChain securityFilterChain(HttpSecurity http, JWTAuthenticationFilter jwtAuthenticationFilter) throws Exception { http + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .csrf(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) @@ -59,6 +65,33 @@ SecurityFilterChain securityFilterChain(HttpSecurity http, JWTAuthenticationFilt return http.build(); } + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + // 로컬 프론트, 로컬 스웨거, 본 서버 도메인 모두 추가 + configuration.setAllowedOriginPatterns(List.of( + "http://localhost:3000", + "http://localhost:3001", + "http://localhost:8080", // 로컬 테스트용 스웨거 (포트 8080일 경우) + "http://localhost:8081", // 로컬 테스트용 스웨거 (포트 8081일 경우) + "https://compasser.site", // 본 서버 스웨거 및 배포된 프론트엔드 도메인 + "https://www.compasser.site", // www가 붙은 도메인 + "https://owner.compasser.co.kr/" + )); + + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + + // 모든 헤더 허용 + configuration.setAllowedHeaders(List.of("*")); + + // 쿠키(인증 정보) 전송 허용 + configuration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { diff --git a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/jwt/JWTProvider.java b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/jwt/JWTProvider.java index 23bca37..33030f4 100644 --- a/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/jwt/JWTProvider.java +++ b/src/main/java/Comprehensive_Design_Project/CUK_Compasser/global/security/jwt/JWTProvider.java @@ -22,9 +22,9 @@ @Component public class JWTProvider { - // 일단 1시간으로 세팅 - private static final Long ACCESS_TOKEN_EXPIRE_TIME = (long) 1000 * 60 * 60; - private static final Long REFRESH_TOKEN_EXPIRE_TIME = (long) 1000 * 60 * 60; + // 일단 1시간으로 세팅 -> access token만 30분 설정, refresh token은 1주일로 설정 + private static final Long ACCESS_TOKEN_EXPIRE_TIME = (long) 1000 * 60 * 30; + private static final Long REFRESH_TOKEN_EXPIRE_TIME = (long) 1000 * 60 * 60 * 24 * 7; private final Key key; private final CustomUserDetailsService customUserDetailsService; diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 7835e2d..7a5e55c 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -27,10 +27,10 @@ spring: - account_email datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/${MYSQL_DB_NAME} - username: ${MYSQL_USERNAME} + url: jdbc:mysql://localhost:3306/${DB_NAME:cuk_compasser}?useSSL=false&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&characterEncoding=UTF-8 + username: root password: ${MYSQL_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 5 @@ -108,7 +108,7 @@ kakao: kakaopay: cid: TC0ONETIME - secret-key: ${KAKAOPAY_SECRET_KEY_DEV} + secret-key: ${KAKAOPAY_SECRET_KEY_DEV:local-dummy} base-url: https://open-api.kakaopay.com approval-url: https://compasser.co.kr/payments/kakaopay/success cancel-url: https://compasser.co.kr/payments/kakaopay/cancel @@ -123,4 +123,4 @@ springdoc: nts: business: base-url: https://api.odcloud.kr/api/nts-businessman/v1 - service-key: ${NTS_SERVICE_KEY} \ No newline at end of file + service-key: ${NTS_SERVICE_KEY:local-dummy} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 1c6cf1f..21374d1 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -35,9 +35,18 @@ spring: idle-timeout: 600000 max-lifetime: 1800000 pool-name: HikariPool-1 + # 다음 배포 때는 never로 바꿔서 중복 삽입 에러 방지 + sql: + init: + mode: always + data-locations: + - classpath:sql/UserData.sql + - classpath:sql/StoreDataInsertSQL.sql + - classpath:sql/RandomBoxDataInsertSQL.sql jpa: database-platform: org.hibernate.dialect.MySQLDialect + defer-datasource-initialization: true hibernate: ddl-auto: update show-sql: false @@ -96,4 +105,23 @@ logging: max-file-size: 10MB level: root: INFO - org.hibernate.SQL: ERROR \ No newline at end of file + org.hibernate.SQL: ERROR + +kakaopay: + cid: TC0ONETIME + secret-key: ${KAKAOPAY_SECRET_KEY_DEV} + base-url: https://open-api.kakaopay.com + approval-url: https://compasser.co.kr/payments/kakaopay/success + cancel-url: https://compasser.co.kr/payments/kakaopay/cancel + fail-url: https://compasser.co.kr/payments/kakaopay/fail + +app: + frontend-base-url: https://www.compasser.co.kr + +springdoc: + packages-to-scan: Comprehensive_Design_Project.CUK_Compasser + +nts: + business: + base-url: https://api.odcloud.kr/api/nts-businessman/v1 + service-key: ${NTS_SERVICE_KEY} \ No newline at end of file diff --git a/src/main/resources/sql/RandomBoxDataInsertSQL.sql b/src/main/resources/sql/RandomBoxDataInsertSQL.sql index e25bed5..075596f 100644 --- a/src/main/resources/sql/RandomBoxDataInsertSQL.sql +++ b/src/main/resources/sql/RandomBoxDataInsertSQL.sql @@ -1,4 +1,4 @@ -INSERT INTO random_boxes ( +INSERT IGNORE INTO random_boxes ( created_at, updated_at, store_id, box_name, content, stock, price, buy_limit, sale_status ) VALUES diff --git a/src/main/resources/sql/StoreDataInsertSQL.sql b/src/main/resources/sql/StoreDataInsertSQL.sql index 0b54d64..b391a79 100644 --- a/src/main/resources/sql/StoreDataInsertSQL.sql +++ b/src/main/resources/sql/StoreDataInsertSQL.sql @@ -3,7 +3,7 @@ -- 일반 데이터 기준: 37.5445, 127.0560 -- 가톨릭대 정문: 37.4855, 126.8025 -- --------------------------------------------------------- -INSERT INTO stores (latitude, longitude, created_at, updated_at, +INSERT IGNORE INTO stores (latitude, longitude, created_at, updated_at, store_manager_id, store_name, store_details, business_hours, input_address, jibun_address, road_address, store_email, tag diff --git a/src/main/resources/sql/UserData.sql b/src/main/resources/sql/UserData.sql index 73bd33d..520cf59 100644 --- a/src/main/resources/sql/UserData.sql +++ b/src/main/resources/sql/UserData.sql @@ -1,7 +1,7 @@ -- --------------------------------------------------------- -- 1. members 데이터 삽입 (ID 1~40) -- --------------------------------------------------------- -INSERT INTO members (created_at, updated_at, email, member_name, password, phone, provider, provider_id, role, status) +INSERT IGNORE INTO members (created_at, updated_at, email, member_name, password, phone, provider, provider_id, role, status) VALUES (NOW(), NOW(), 'manager1@test.com', '성수매니저', 'password', '010-1111-1111', 'NORMAL', NULL, 'STORE_MANAGER', 'ACTIVE'), (NOW(), NOW(), 'manager2@test.com', '강남매니저', 'password', '010-2222-2222', 'NORMAL', NULL, 'STORE_MANAGER','ACTIVE'), (NOW(), NOW(), 'manager3@test.com', '연남매니저', 'password', '010-3333-3333', 'NORMAL', NULL, 'STORE_MANAGER','ACTIVE'), @@ -46,7 +46,7 @@ VALUES (NOW(), NOW(), 'manager1@test.com', '성수매니저', 'password', '010-1 -- --------------------------------------------------------- -- 2. store_managers 데이터 삽입 (member_id와 1:1 매칭) -- --------------------------------------------------------- -INSERT INTO store_managers (created_at, member_id, updated_at, verified_at, business_license_number) +INSERT IGNORE INTO store_managers (created_at, member_id, updated_at, verified_at, business_license_number) VALUES (NOW(), 1, NOW(), NOW(), '111-81-12345'), (NOW(), 2, NOW(), NOW(), '222-82-23456'), (NOW(), 3, NOW(), NOW(), '333-83-34567'),