Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example.konnect_backend.domain.admin.controller;

import com.example.konnect_backend.domain.admin.dto.request.AdminLoginRequest;
import com.example.konnect_backend.domain.admin.service.AdminAuthService;
import com.example.konnect_backend.domain.auth.dto.response.AuthResponse;
import com.example.konnect_backend.global.ApiResponse;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
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/admin/auth")
@RequiredArgsConstructor
@Hidden
@Tag(name = "Admin Authentication", description = "관리자 인증 (스펙 비노출)")
public class AdminAuthController {

private final AdminAuthService adminAuthService;

@PostMapping("/login")
@Operation(summary = "관리자 로그인", description = "요청 JSON의 id(로그인 ID)·password 검증 후 ADMIN 역할 JWT 액세스 토큰을 발급합니다.")
@ApiResponses(value = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content(schema = @Schema(implementation = ApiResponse.class))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "계정 없음 또는 비밀번호 불일치", content = @Content(schema = @Schema(implementation = ApiResponse.class)))
Comment thread
Jaehyeon-Han marked this conversation as resolved.
})
public ApiResponse<AuthResponse> login(@Valid @RequestBody AdminLoginRequest request) {
return ApiResponse.onSuccess(adminAuthService.login(request));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.konnect_backend.domain.ai.controller;
package com.example.konnect_backend.domain.admin.controller;

import com.example.konnect_backend.domain.ai.domain.vo.PipelineContext;
import com.example.konnect_backend.domain.ai.domain.vo.TextExtractionResult;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.konnect_backend.domain.admin.dto.request;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@Schema(description = "관리자 로그인 요청")
public class AdminLoginRequest {

@NotBlank
@JsonProperty("id")
@Schema(description = "로그인 ID", example = "root")
private String loginId;

@NotBlank
@Schema(description = "비밀번호")
private String password;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.example.konnect_backend.domain.admin.entity;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Entity
@Table(name = "admin")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Admin {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "login_id", nullable = false, unique = true, length = 64)
private String loginId;

@Column(nullable = false)
private String password;

@Column(length = 100)
private String name;

@Column(name = "created_at", nullable = false, updatable = false, insertable = false)
private LocalDateTime createdAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.konnect_backend.domain.admin.repository;

import com.example.konnect_backend.domain.admin.entity.Admin;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface AdminRepository extends JpaRepository<Admin, Long> {

Optional<Admin> findByLoginId(String loginId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.example.konnect_backend.domain.admin.service;

import com.example.konnect_backend.domain.admin.dto.request.AdminLoginRequest;
import com.example.konnect_backend.domain.admin.entity.Admin;
import com.example.konnect_backend.domain.admin.repository.AdminRepository;
import com.example.konnect_backend.domain.auth.dto.response.AuthResponse;
import com.example.konnect_backend.global.code.status.ErrorStatus;
import com.example.konnect_backend.global.exception.GeneralException;
import com.example.konnect_backend.global.security.JwtTokenProvider;
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 AdminAuthService {

private final AdminRepository adminRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;

public AuthResponse login(AdminLoginRequest request) {
Admin admin = adminRepository.findByLoginId(request.getLoginId())
.orElseThrow(() -> new GeneralException(ErrorStatus.PASSWORD_FAILED));

if (!passwordEncoder.matches(request.getPassword(), admin.getPassword())) {
throw new GeneralException(ErrorStatus.PASSWORD_FAILED);
}

String accessToken = jwtTokenProvider.createToken(admin.getId(), "ADMIN");
return AuthResponse.of(accessToken, admin.getId(), "ADMIN");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,15 @@ public Object saveModuleNameAndPromptVersionInContext(ProceedingJoinPoint joinPo
public Object logGeminiCall(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
PromptContext promptContext = PromptContextHolder.get();
String requestIdString = MDC.get(REQUEST_ID_KEY); // 모듈 자체를 비동기로 호출 시 주입 또는 전파 필요
UUID requestId = UUID.fromString(requestIdString);
String requestIdString = MDC.get(REQUEST_ID_KEY);
UUID requestId;
try {
requestId = (requestIdString == null || requestIdString.isBlank())
? UUID.randomUUID()
: UUID.fromString(requestIdString);
} catch (IllegalArgumentException e) {
requestId = UUID.randomUUID();
}

Comment thread
Jaehyeon-Han marked this conversation as resolved.
try {
GeminiCallResult callResult = (GeminiCallResult) joinPoint.proceed();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,22 @@ public void saveLog(UUID requestId, @Nullable GeminiCallResult result, PromptCon
moduleName, result.finishReason(), logTime);
}

LlmCallMetadata saved = metadataRepository.save(metadata);
LlmCallMetadata saved = null;

try {
saved = metadataRepository.saveAndFlush(metadata);
} catch (Exception e) {
log.error("metadata save 실패",
kv("request id", requestId),
kv("module name", moduleName),
kv("prompt version", promptVersion),
kv("error", e.getMessage()),
e
);
}
Comment thread
Jaehyeon-Han marked this conversation as resolved.

// 프롬프트 템플릿 정보 및 입력 변수와 모델 응답 로깅
log.info("Gemini API 호출 완료",
kv("metadata id", saved.getId()),
kv("metadata id", saved != null ? saved.getId() : null),
kv("request id", requestId),
Comment thread
Jaehyeon-Han marked this conversation as resolved.
kv("model response", result == null ? null : result.response()),
kv("module name", moduleName),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,12 @@ public void activate(Long promptId) {
}
PromptTemplate previousActive = activePrompts.get(0);

toActivate.setStatus(PromptStatus.ACTIVE);
previousActive.setStatus(PromptStatus.DEPRECATED);
promptRepository.flush();
toActivate.setStatus(PromptStatus.ACTIVE);
Comment thread
Jaehyeon-Han marked this conversation as resolved.
}

@Transactional
public RunResultResponse run(RunPromptRequest request) {
String prompt = resolver.resolve(request.promptTemplate(), request.vars());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public OpenAPI openAPI() {

// 서버 URL 명시 (http 대신 https)
Server productionServer = new Server()
.url("https://api.konnect-women.com")
.url("https://api.women-konnect.com")
.description("Production Server (HTTPS)");

Server localServer = new Server()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/index.html", "/static/**", "/favicon.ico").permitAll()
.requestMatchers("/", "/index.html", "/assets/**", "/favicon.ico").permitAll()
// Swagger UI 관련 모든 경로 허용
.requestMatchers(
"/swagger-ui.html",
Expand All @@ -58,12 +58,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
"/webjars/**"
).permitAll()
.requestMatchers("/api/auth/**", "/api/schools/**", "/api/device/**", "/api/ai/**", "/api/usage/**", "/api/message/**", "/api/users/language").permitAll()
.requestMatchers("/api/admin/**").denyAll() // Todo 관리자만 허용 필요
.requestMatchers(HttpMethod.POST, "/api/admin/auth/login").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/ws/**", "/ws/**").permitAll()
.requestMatchers("/login/oauth2/**", "/oauth2/**").permitAll()
.requestMatchers("/public/**").permitAll()
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers("/api/ai/analyze").permitAll() // 로컬 테스트 편의를 위해 허용
.requestMatchers("/api/ai/analyze").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(o -> o
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# JPA Configuration
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.jpa.properties.hibernate.format_sql=true

Expand Down
12 changes: 12 additions & 0 deletions src/main/resources/db/migration/V25__add_admin_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE `admin`
(
id BIGINT NOT NULL AUTO_INCREMENT,
login_id VARCHAR(64) NOT NULL,
password VARCHAR(255) NOT NULL,
name VARCHAR(100) NULL,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id),
UNIQUE KEY uk_admin_login_id (login_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci;
Loading
Loading