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
Expand Up @@ -7,6 +7,7 @@
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import repit.repit_api_server.domain.metadata.dto.response.GenerateResponse;
import repit.repit_api_server.domain.metadata.dto.response.MaterialsResponse;
import repit.repit_api_server.domain.metadata.dto.response.MetaDataResponse;
import repit.repit_api_server.domain.metadata.service.AnalysisLaunchService;
import repit.repit_api_server.domain.metadata.service.MetaService;
Expand Down Expand Up @@ -42,6 +43,14 @@ public ResponseEntity<MetaDataResponse> getMetaData(
return ResponseEntity.ok(metaService.getMetaData(authUser.token()));
}

// 마이페이지 편집 화면이 처음 채울 저장값. 저장(PUT)은 비개발 API 명세(03 §3)를 받은 뒤 붙인다.
@GetMapping("/materials")
public ResponseEntity<MaterialsResponse> getMaterials(
@AuthenticationPrincipal AuthUser authUser
) {
return ResponseEntity.ok(metaService.getMaterials(authUser.token()));
}


// 자료를 한 번도 올리지 않은 사용자는 인증 서버가 두 값을 null로 준다. 없는 것이지 오류가 아니다.
@GetMapping("/isGit")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package repit.repit_api_server.domain.metadata.dto.response;

import java.util.List;

/**
* 인증 서버 {@code GET /api/v1/auth/materials} 응답(계약 C-2).
*
* <p>major는 MAJOR_ 접두사가 붙은 저장값이다. legacyPortfolio는 새 형식으로 저장한 적 없는
* 옛 사용자의 PDF 주소이고, 그때 documents는 빈 목록으로 온다.
*/
public record AuthMaterialsResponse(
String major,
List<DocumentEntry> documents,
List<String> gitUrls,
List<SupplementEntry> supplements,
String legacyPortfolio) {

public record DocumentEntry(String type, String fileUrl, String fileName, Integer pageCount) {
}

public record SupplementEntry(String title, Integer documentIndex,
String goal, String role, String decision, String outcome) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package repit.repit_api_server.domain.metadata.dto.response;

import repit.repit_api_server.domain.userdata.persona.support.UserJobMajors;

import java.util.ArrayList;
import java.util.List;

/**
* {@code GET /api/v1/metaData/materials} 응답(계약 C-1). 목록은 null 대신 빈 목록이다.
*
* <p>jobRole은 MAJOR_ 접두사를 뗀 직무 코드이고, 직무를 고르지 않았으면 null이다.
* 문서 순서가 곧 index이고, 보완 입력의 documentIndex가 이 값을 가리킨다.
*/
public record MaterialsResponse(
String jobRole,
List<MaterialDocument> documents,
List<String> gitUrls,
List<Supplement> supplements) {

/** 옛 업로드 경로(dataUpload)로 올린 PDF는 이 종류의 문서 한 건으로 보여 준다. */
static final String LEGACY_DOCUMENT_TYPE = "PORTFOLIO";

public record MaterialDocument(int index, String type, String fileUrl, String fileName, Integer pageCount) {
}

public record Supplement(String title, Integer documentIndex,
String goal, String role, String decision, String outcome) {
}

public static MaterialsResponse empty() {
return new MaterialsResponse(null, List.of(), List.of(), List.of());
}

public static MaterialsResponse from(AuthMaterialsResponse auth) {
if (auth == null) {
return empty();
}
return new MaterialsResponse(
UserJobMajors.normalize(auth.major()),
documentsOf(auth),
auth.gitUrls() == null ? List.of() : auth.gitUrls(),
auth.supplements() == null ? List.of() : auth.supplements().stream()
.map(s -> new Supplement(s.title(), s.documentIndex(), s.goal(), s.role(), s.decision(), s.outcome()))
.toList());
}

private static List<MaterialDocument> documentsOf(AuthMaterialsResponse auth) {
List<AuthMaterialsResponse.DocumentEntry> saved = auth.documents() == null ? List.of() : auth.documents();
if (saved.isEmpty()) {
// 옛 사용자는 파일명·쪽수를 저장한 적이 없어 null로 둔다.
return auth.legacyPortfolio() == null || auth.legacyPortfolio().isBlank()
? List.of()
: List.of(new MaterialDocument(0, LEGACY_DOCUMENT_TYPE, auth.legacyPortfolio(), null, null));
}
List<MaterialDocument> documents = new ArrayList<>(saved.size());
for (int i = 0; i < saved.size(); i++) {
AuthMaterialsResponse.DocumentEntry d = saved.get(i);
documents.add(new MaterialDocument(i, d.type(), d.fileUrl(), d.fileName(), d.pageCount()));
}
return List.copyOf(documents);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.web.multipart.MultipartFile;
import repit.repit_api_server.global.client.AuthServerClient;
import repit.repit_api_server.domain.metadata.dto.request.MetaDataRequest;
import repit.repit_api_server.domain.metadata.dto.response.MaterialsResponse;
import repit.repit_api_server.domain.metadata.dto.response.MetaDataResponse;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
Expand Down Expand Up @@ -58,6 +59,11 @@ public MetaDataResponse getMetaData(String token) {
return authServerClient.getMetaData(token);
}

// 직무·문서·GitHub·보완 입력 반환. 옛 업로드 경로로 올린 PDF는 PORTFOLIO 문서 한 건으로 바꾼다.
public MaterialsResponse getMaterials(String token) {
return MaterialsResponse.from(authServerClient.getMaterials(token));
}

// S3 file 업로드
private String uploadFile(MultipartFile file) throws IOException {
// 원본 파일명과 확장자 추출
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.PostExchange;
import repit.repit_api_server.domain.metadata.dto.request.MetaDataRequest;
import repit.repit_api_server.domain.metadata.dto.response.AuthMaterialsResponse;
import repit.repit_api_server.domain.metadata.dto.response.MetaDataResponse;
import repit.repit_api_server.global.common.ApiResponse;
import repit.repit_api_server.global.response.UserResponse;
Expand All @@ -18,6 +19,9 @@ void createMetaData(@RequestHeader("Authorization") String authorization,
@GetExchange("/api/v1/auth/getMetaData")
ApiResponse<MetaDataResponse> getMetaData(@RequestHeader("Authorization") String authorization);

@GetExchange("/api/v1/auth/materials")
ApiResponse<AuthMaterialsResponse> getMaterials(@RequestHeader("Authorization") String authorization);

@GetExchange("/api/v1/users/me")
ApiResponse<UserResponse> getUser(@RequestHeader("Authorization") String authorization);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.springframework.http.HttpStatusCode;
import org.springframework.stereotype.Component;
import repit.repit_api_server.domain.metadata.dto.request.MetaDataRequest;
import repit.repit_api_server.domain.metadata.dto.response.AuthMaterialsResponse;
import repit.repit_api_server.global.common.ApiResponse;
import repit.repit_api_server.domain.metadata.dto.response.MetaDataResponse;
import repit.repit_api_server.global.response.UserResponse;
Expand Down Expand Up @@ -36,6 +37,15 @@ public MetaDataResponse getMetaData(String authorization) {
return response == null ? null : response.getData();
}

// Auth 서버에서 직무·문서·GitHub·보완 입력 반환
public AuthMaterialsResponse getMaterials(String authorization) {
ApiResponse<AuthMaterialsResponse> response = executor.execute(SERVER_NAME,
() -> authServerApi.getMaterials(authorization),
this::resolveMessage, true);

return response == null ? null : response.getData();
}

public UserResponse getUser(String authorization) {
ApiResponse<UserResponse> response = executor.execute(SERVER_NAME,
() -> authServerApi.getUser(authorization),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import repit.repit_api_server.domain.metadata.dto.response.MaterialsResponse;
import repit.repit_api_server.domain.metadata.dto.response.MetaDataResponse;
import repit.repit_api_server.domain.metadata.service.AnalysisLaunchService;
import repit.repit_api_server.domain.metadata.service.MetaService;
Expand Down Expand Up @@ -55,4 +56,14 @@ class MetaDataControllerTest {
assertThat(controller.isGit(USER).getBody()).isTrue();
assertThat(controller.isPortfolio(USER).getBody()).isTrue();
}

@Test
void 저장된_직무와_자료를_사용자_토큰으로_받아_그대로_돌려준다() {
MaterialsResponse materials = new MaterialsResponse("QA_ENGINEER",
List.of(new MaterialsResponse.MaterialDocument(0, "TEST_PLAN", "https://s3/plan.pdf", "plan.pdf", 12)),
List.of(), List.of());
when(metaService.getMaterials("Bearer token")).thenReturn(materials);

assertThat(controller.getMaterials(USER).getBody()).isSameAs(materials);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package repit.repit_api_server.domain.metadata.dto.response;

import org.junit.jupiter.api.Test;
import repit.repit_api_server.domain.metadata.dto.response.AuthMaterialsResponse.DocumentEntry;
import repit.repit_api_server.domain.metadata.dto.response.AuthMaterialsResponse.SupplementEntry;
import repit.repit_api_server.domain.metadata.dto.response.MaterialsResponse.MaterialDocument;
import repit.repit_api_server.domain.metadata.dto.response.MaterialsResponse.Supplement;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

/**
* 인증 서버의 저장값(C-2)을 마이페이지 응답(C-1)으로 바꾸는 규칙.
*/
class MaterialsResponseTest {

@Test
void 새_형식으로_저장한_자료는_접두사를_떼고_문서_순서대로_index를_붙인다() {
AuthMaterialsResponse auth = new AuthMaterialsResponse(
"MAJOR_PRODUCT_MANAGER",
List.of(new DocumentEntry("PRD", "https://s3/prd.pdf", "회원가입-PRD.pdf", 9),
new DocumentEntry("POLICY_DOC", "https://s3/policy.pdf", "정책서.pdf", 6)),
List.of(),
List.of(new SupplementEntry("회원가입 개편", 0, "이탈 줄이기", "정책 정리", "단계 축소", null)),
null);

MaterialsResponse response = MaterialsResponse.from(auth);

assertThat(response.jobRole()).isEqualTo("PRODUCT_MANAGER");
assertThat(response.documents()).containsExactly(
new MaterialDocument(0, "PRD", "https://s3/prd.pdf", "회원가입-PRD.pdf", 9),
new MaterialDocument(1, "POLICY_DOC", "https://s3/policy.pdf", "정책서.pdf", 6));
assertThat(response.gitUrls()).isEmpty();
assertThat(response.supplements()).containsExactly(
new Supplement("회원가입 개편", 0, "이탈 줄이기", "정책 정리", "단계 축소", null));
}

@Test
void 옛_사용자의_PDF는_파일명과_쪽수가_없는_PORTFOLIO_문서_한_건이다() {
AuthMaterialsResponse auth = new AuthMaterialsResponse(
null, List.of(), List.of("https://github.com/a/repo"), List.of(), "https://s3/legacy.pdf");

MaterialsResponse response = MaterialsResponse.from(auth);

assertThat(response.jobRole()).isNull();
assertThat(response.documents()).containsExactly(
new MaterialDocument(0, "PORTFOLIO", "https://s3/legacy.pdf", null, null));
assertThat(response.gitUrls()).containsExactly("https://github.com/a/repo");
}

@Test
void 아무것도_저장하지_않았으면_직무는_null이고_목록은_비어_있다() {
AuthMaterialsResponse auth = new AuthMaterialsResponse(null, null, null, null, null);

MaterialsResponse response = MaterialsResponse.from(auth);

assertThat(response).isEqualTo(MaterialsResponse.empty());
assertThat(response.documents()).isEmpty();
assertThat(response.gitUrls()).isEmpty();
assertThat(response.supplements()).isEmpty();
}

@Test
void 빈_문자열_PDF_주소는_문서로_보지_않는다() {
AuthMaterialsResponse auth = new AuthMaterialsResponse("MAJOR_BACKEND", List.of(), List.of(), List.of(), " ");

assertThat(MaterialsResponse.from(auth).documents()).isEmpty();
}

@Test
void 인증_서버가_빈_응답을_주면_빈_자료다() {
assertThat(MaterialsResponse.from(null)).isEqualTo(MaterialsResponse.empty());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package repit.repit_api_server.global.client;

import com.sun.net.httpserver.HttpServer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.web.client.RestClient;
import repit.repit_api_server.domain.metadata.dto.response.AuthMaterialsResponse;
import repit.repit_api_server.domain.metadata.dto.response.AuthMaterialsResponse.DocumentEntry;
import repit.repit_api_server.domain.metadata.dto.response.AuthMaterialsResponse.SupplementEntry;
import repit.repit_api_server.global.config.HttpInterfaceConfig;

import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;

import static org.assertj.core.api.Assertions.assertThat;

/**
* 인증 서버 {@code GET /api/v1/auth/materials}(계약 C-2)의 실제 응답 모양을 설정으로 만든 클라이언트가 읽는지 확인한다.
* 인증 서버는 응답에 message·null 필드를 함께 보내므로 그대로 흉내 낸다.
*/
class AuthServerMaterialsWiringTest {

private static final String RESPONSE_BODY = """
{"success": true, "message": null, "data": {
"major": "MAJOR_QA_ENGINEER",
"documents": [{"type": "TEST_PLAN", "fileUrl": "https://s3/plan.pdf", "fileName": "결제-테스트계획.pdf", "pageCount": 12}],
"gitUrls": ["https://github.com/a/qa"],
"supplements": [{"title": "결제 QA", "documentIndex": 0, "goal": "중복 결제 막기", "role": null, "decision": null, "outcome": null}],
"legacyPortfolio": null}}
""";

private HttpServer authServer;
private final AtomicReference<String> receivedAuthorization = new AtomicReference<>();

@BeforeEach
void 가짜_인증_서버를_띄운다() throws Exception {
authServer = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
authServer.createContext("/api/v1/auth/materials", exchange -> {
receivedAuthorization.set(exchange.getRequestHeaders().getFirst("Authorization"));
byte[] body = RESPONSE_BODY.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "application/json");
exchange.sendResponseHeaders(200, body.length);
try (OutputStream out = exchange.getResponseBody()) {
out.write(body);
}
});
authServer.start();
}

@AfterEach
void 정리한다() {
authServer.stop(0);
}

@Test
void 사용자_토큰을_넘기고_직무_문서_GitHub_보완_입력을_읽는다() {
contextRunner().run(context -> {
AuthServerClient client = new AuthServerClient(context.getBean(AuthServerApi.class), new ExternalApiExecutor());

AuthMaterialsResponse response = client.getMaterials("Bearer user-token");

assertThat(receivedAuthorization.get()).isEqualTo("Bearer user-token");
assertThat(response.major()).isEqualTo("MAJOR_QA_ENGINEER");
assertThat(response.documents()).containsExactly(
new DocumentEntry("TEST_PLAN", "https://s3/plan.pdf", "결제-테스트계획.pdf", 12));
assertThat(response.gitUrls()).containsExactly("https://github.com/a/qa");
assertThat(response.supplements()).containsExactly(
new SupplementEntry("결제 QA", 0, "중복 결제 막기", null, null, null));
assertThat(response.legacyPortfolio()).isNull();
});
}

private ApplicationContextRunner contextRunner() {
String baseUrl = "http://127.0.0.1:" + authServer.getAddress().getPort();
return new ApplicationContextRunner()
.withBean("restClientBuilder", RestClient.Builder.class, RestClient::builder)
.withUserConfiguration(HttpInterfaceConfig.class)
.withPropertyValues(
"ai-server.base-url=" + baseUrl,
"auth-server.base-url=" + baseUrl,
"chat-server.base-url=" + baseUrl
);
}
}
Loading