Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
11 commits
Select commit Hold shift + click to select a range
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 @@ -139,7 +139,8 @@ public void updateMeetingTitle(User user, Meeting meeting, MeetingRequestDTO.upd

meeting.updateTitle(request.getTitle());

markdownFileUploader.updateFileTitle(meeting.getProceedingPdfKeyName(), request.getTitle());
markdownFileUploader.updateFileTitle(meeting.getProceedingPdfKeyName(), request.getTitle() + ".pdf");
markdownFileUploader.updateFileTitle(meeting.getProceedingWordKeyName(), request.getTitle() + ".docx");

meetingRepository.save(meeting);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ public ApiResponse<MoodTrackerResponseDTO.ReportDownLoadLinkResponse> downloadLi

@PostMapping("/{mood-tracker-hashed-Id}/report-test")
@Operation(
summary = "λΆ„μœ„κΈ° 트래컀 μ„€λ¬Έ 리포트 μ¦‰μ‹œ 생성 ν…ŒμŠ€νŠΈ API",
summary = "λΆ„μœ„κΈ° 트래컀 μ„€λ¬Έ 리포트 μ¦‰μ‹œ 생성 API",
description = "# [v1.0 (2025-07-26)](https://www.notion.so/23f5da7802c58080b4a5e6d24b47d924) ν•΄λ‹Ή ID의 λΆ„μœ„κΈ° 트래컀 μ„€λ¬Έ 리포트λ₯Ό μ¦‰μ‹œ μƒμ„±ν•©λ‹ˆλ‹€."
)
@Parameters({
Expand All @@ -274,7 +274,7 @@ public ApiResponse<Void> generateMoodTrackerReportTest (

@PostMapping("/{mood-tracker-hashed-Id}/report-file-thumbnail-test")
@Operation(
summary = "λΆ„μœ„κΈ° 트래컀 μ„€λ¬Έ 리포트, 파일, 썸넀일 μ¦‰μ‹œ 생성 ν…ŒμŠ€νŠΈ API",
summary = "λΆ„μœ„κΈ° 트래컀 μ„€λ¬Έ 리포트, 파일, 썸넀일 μ¦‰μ‹œ 생성 ν…ŒμŠ€νŠΈ API + redisμ—μ„œ μ œμ™Έν•˜μ—¬ λ§ˆκ°μΌμ‹œμ— 쀑볡 생성 λΆˆκ°€",
description = "# [v1.0 (2025-08-14)](https://www.notion.so/24f5da7802c58019a1f7d9c8e882226e) ν•΄λ‹Ή ID의 λΆ„μœ„κΈ° 트래컀 μ„€λ¬Έ 리포트λ₯Ό μ¦‰μ‹œ μƒμ„±ν•©λ‹ˆλ‹€."
)
@Parameters({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.haru.api.global.apiPayload.code.status.ErrorStatus;
import com.haru.api.global.apiPayload.exception.handler.*;
import com.haru.api.global.util.HashIdUtil;
import com.haru.api.infra.redis.RedisReportConsumer;
import com.haru.api.infra.redis.RedisReportProducer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -52,6 +53,7 @@ public class MoodTrackerCommandServiceImpl implements MoodTrackerCommandService

private final MoodTrackerReportService moodTrackerReportService;
private final RedisReportProducer redisReportProducer;
private final RedisReportConsumer redisReportConsumer;

private final HashIdUtil hashIdUtil;

Expand Down Expand Up @@ -318,6 +320,10 @@ public void generateReportTest(
public void generateReportFileAndThumbnailTest(
MoodTracker moodTracker
) {
// 쀑볡 처리 μ œμ™Έ
redisReportConsumer.removeFromQueue(moodTracker.getId());

// μ¦‰μ‹œ 생성
moodTrackerReportService.generateAndUploadReportFileAndThumbnail(moodTracker.getId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.haru.api.domain.snsEvent.dto.SnsEventResponseDTO;
import com.haru.api.domain.snsEvent.entity.SnsEvent;
import com.haru.api.domain.snsEvent.entity.enums.Format;
import com.haru.api.domain.snsEvent.entity.enums.InstagramRedirectType;
import com.haru.api.domain.snsEvent.entity.enums.ListType;
import com.haru.api.domain.snsEvent.service.SnsEventCommandService;
import com.haru.api.domain.snsEvent.service.SnsEventQueryService;
Expand Down Expand Up @@ -56,19 +57,20 @@ public ApiResponse<?> instagramRedirectUri(
}

@Operation(
summary = "μΈμŠ€νƒ€κ·Έλž¨ 연동 API [v1.1 (2025-08-07)]",
description = "# [v1.1 (2025-08-07)](https://www.notion.so/API-21e5da7802c581cca23dff937ac3f155?p=23f5da7802c5803b98abe74d511c2cf4&pm=s)" +
summary = "μΈμŠ€νƒ€κ·Έλž¨ 연동 API [v1.1 (2025-08-21)]",
description = "# [v1.1 (2025-08-21)](https://www.notion.so/API-21e5da7802c581cca23dff937ac3f155?p=23f5da7802c5803b98abe74d511c2cf4&pm=s)" +
" μΈμŠ€νƒ€κ·Έλž¨ 둜그인 ν›„ 인증 μ„œλ²„λ‘œλΆ€ν„° 받은 codeλ₯Ό header에 λ„£μ–΄μ£Όμ‹œκ³ , workspaceIdλ₯Ό Path Variable둜 λ„£μ–΄μ£Όμ„Έμš”."
)
@PostMapping("/{workspaceId}/link-instagram")
public ApiResponse<SnsEventResponseDTO.LinkInstagramAccountResponse> linkInstagramAccount(
@RequestHeader("code") String code,
@PathVariable String workspaceId,
@RequestParam InstagramRedirectType instagramRedirectType,
@Parameter(hidden = true) @AuthWorkspace Workspace workspace
) {
System.out.println("Received accessToken: " + code);
return ApiResponse.onSuccess(
snsEventCommandService.getInstagramAccessTokenAndAccount(code, workspace)
snsEventCommandService.getInstagramAccessTokenAndAccount(code, workspace, instagramRedirectType)
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.haru.api.domain.snsEvent.entity.enums;

public enum InstagramRedirectType {
ONBOARDING, WORKSPACE
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
import com.haru.api.domain.snsEvent.dto.SnsEventResponseDTO;
import com.haru.api.domain.snsEvent.entity.SnsEvent;
import com.haru.api.domain.snsEvent.entity.enums.Format;
import com.haru.api.domain.snsEvent.entity.enums.InstagramRedirectType;
import com.haru.api.domain.snsEvent.entity.enums.ListType;
import com.haru.api.domain.user.entity.User;
import com.haru.api.domain.workspace.entity.Workspace;

public interface SnsEventCommandService {
SnsEventResponseDTO.CreateSnsEventResponse createSnsEvent(User user, Workspace workspace, SnsEventRequestDTO.CreateSnsRequest request);

SnsEventResponseDTO.LinkInstagramAccountResponse getInstagramAccessTokenAndAccount(String code, Workspace workspace);
SnsEventResponseDTO.LinkInstagramAccountResponse getInstagramAccessTokenAndAccount(String code, Workspace workspace, InstagramRedirectType instagramRedirectType);

void updateSnsEventTitle(User user, SnsEvent snsEvent, SnsEventRequestDTO.UpdateSnsEventRequest request);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.haru.api.domain.snsEvent.entity.SnsEvent;
import com.haru.api.domain.snsEvent.entity.Winner;
import com.haru.api.domain.snsEvent.entity.enums.Format;
import com.haru.api.domain.snsEvent.entity.enums.InstagramRedirectType;
import com.haru.api.domain.snsEvent.entity.enums.ListType;
import com.haru.api.domain.snsEvent.repository.ParticipantRepository;
import com.haru.api.domain.snsEvent.repository.SnsEventRepository;
Expand Down Expand Up @@ -170,14 +171,18 @@ public SnsEventResponseDTO.CreateSnsEventResponse createSnsEvent(
@Transactional
public SnsEventResponseDTO.LinkInstagramAccountResponse getInstagramAccessTokenAndAccount(
String code,
Workspace workspace
Workspace workspace,
InstagramRedirectType instagramRedirectType
) {
String shortLivedAccessToken;
String longLivedAccessToken;
Map<String, Object> userInfo;
try {
// 1. Access Token μš”μ²­
shortLivedAccessToken = instagramOauth2RestTemplate.getShortLivedAccessTokenUrl(code);
shortLivedAccessToken = instagramOauth2RestTemplate.getShortLivedAccessTokenUrl(
code,
instagramRedirectType
);
// 2. 단기 토큰을 μž₯κΈ°(Long-Lived) ν† ν°μœΌλ‘œ κ΅ν™˜
longLivedAccessToken = instagramOauth2RestTemplate.getLongLivedAccessToken(shortLivedAccessToken);
// 3. μž₯κΈ° ν† ν°μœΌλ‘œ μ‚¬μš©μž 계정 정보 μš”μ²­
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ public enum ErrorStatus implements BaseErrorCode {
INVALID_CHOICE_FOR_QUESTION(HttpStatus.BAD_REQUEST, "MOODTRACKER4011", "λΆ„μœ„κΈ° 트래컀 μ§ˆλ¬Έμ— μœ νš¨ν•˜μ§€ μ•Šμ€ μ„ νƒμ§€μž…λ‹ˆλ‹€."),

// 메일 κ΄€λ ¨ μ—λŸ¬
MAIL_SEND_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "MAIL500", "이메일 전솑에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."),
MAIL_SEND_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "MAIL500", "이메일 전솑에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. (μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 이메일일 수 μžˆμŠ΅λ‹ˆλ‹€)"),
MAIL_INVALID_FORMAT(HttpStatus.BAD_REQUEST, "MAIL400", "이메일 ν˜•μ‹μ΄ 잘λͺ»λ˜μ—ˆμŠ΅λ‹ˆλ‹€."),

// SNS 이벀트 κ΄€λ ¨ μ—λŸ¬
SNS_EVENT_NOT_FOUND(HttpStatus.BAD_REQUEST, "SNSEVENT4001", "SNS μ΄λ²€νŠΈκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.haru.api.infra.api.restTemplate;

import com.haru.api.domain.snsEvent.entity.enums.InstagramRedirectType;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
Expand All @@ -21,12 +22,17 @@ public class InstagramOauth2RestTemplate {
private String instagramClientId;
@Value("${instagram.client.secret}")
private String instagramClientSecret;
@Value("${instagram.redirect.uri}")
private String instagramRedirectUri;
@Value("${instagram.redirect.uri-onboarding}")
private String instagramRedirectUriOnboarding;
@Value("${instagram.redirect.uri-workspace}")
private String instagramRedirectUriWorkspace;

private final RestTemplate restTemplate;

public String getShortLivedAccessTokenUrl(String code) {
public String getShortLivedAccessTokenUrl(
String code,
InstagramRedirectType instagramRedirectType
) {
// 1. Access Token μš”μ²­
String tokenUrl = "https://api.instagram.com/oauth/access_token";
RestTemplate restTemplate = new RestTemplate();
Expand All @@ -35,7 +41,13 @@ public String getShortLivedAccessTokenUrl(String code) {
params.add("client_id", instagramClientId); // μΈμŠ€νƒ€ μ•±μ˜ ν΄λΌμ΄μ–ΈνŠΈ ID
params.add("client_secret", instagramClientSecret); // μΈμŠ€νƒ€ μ•±μ˜ ν΄λΌμ΄μ–ΈνŠΈ μ‹œν¬λ¦Ώ
params.add("grant_type", "authorization_code");
params.add("redirect_uri", instagramRedirectUri); // μΈκ°€μ½”λ“œμ™€ λ™μΌν•œ redirect_uri
// μΈκ°€μ½”λ“œμ™€ λ™μΌν•œ redirect_uri
if (instagramRedirectType == InstagramRedirectType.ONBOARDING) {
params.add("redirect_uri", instagramRedirectUriOnboarding);
} else if(instagramRedirectType == InstagramRedirectType.WORKSPACE) {
System.out.println("Using workspace redirect URI: " + instagramRedirectUriWorkspace);
params.add("redirect_uri", instagramRedirectUriWorkspace);
}
params.add("code", code);

HttpHeaders headers = new HttpHeaders();
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/haru/api/infra/mail/SmtpEmailSender.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.haru.api.global.apiPayload.code.status.ErrorStatus;
import com.haru.api.infra.mail.handler.MailHandler;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -27,6 +29,13 @@ public void send(
String title,
String content
) {
// 1. 이메일 ν˜•μ‹ 검증
if (!isValidEmail(to)) {
log.warn("잘λͺ»λœ 이메일 ν˜•μ‹ - {}", to);
throw new MailHandler(ErrorStatus.MAIL_INVALID_FORMAT);
}

// 2. 메일 전솑 μ‹œλ„
try {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, false, "UTF-8");
Expand All @@ -44,5 +53,16 @@ public void send(
throw new MailHandler(ErrorStatus.MAIL_SEND_FAIL);
}
}

// RFC ν˜•μ‹ 검증
private boolean isValidEmail(String email) {
try {
InternetAddress internetAddress = new InternetAddress(email);
internetAddress.validate(); // ν˜•μ‹μ΄ 틀리면 AddressException λ°œμƒ
return true;
} catch (AddressException e) {
return false;
}
}
}

14 changes: 14 additions & 0 deletions src/main/java/com/haru/api/infra/redis/RedisReportConsumer.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,18 @@ public void pollQueueEvery30Minutes() {
}
}
}

@Transactional
public void removeFromQueue(Long moodTrackerId) {
try {
Long removed = redisTemplate.opsForZSet().remove(QUEUE_KEY, moodTrackerId.toString());
if (removed != null && removed > 0) {
log.info("μ¦‰μ‹œ 생성 API 호좜둜 νμ—μ„œ 제거됨: {}", moodTrackerId);
} else {
log.info("큐에 μ‘΄μž¬ν•˜μ§€ μ•Šμ•„ μ œκ±°ν•  ν•­λͺ© μ—†μŒ: {}", moodTrackerId);
}
} catch (Exception e) {
log.error("큐 제거 μ‹€νŒ¨: {}", moodTrackerId, e);
}
}
}
1 change: 1 addition & 0 deletions src/main/java/com/haru/api/infra/s3/AmazonS3Manager.java
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ public byte[] downloadFile(String keyName) {
}
}

// newDisplayName에 ν™•μž₯자 ν¬ν•¨λ˜μ–΄μžˆμŒ
public void updateFileTitle(String keyName, String newDisplayName) {

try {
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/haru/api/infra/s3/MarkdownFileUploader.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public String createOrUpdatePdf(String markdownText, String featurePath, String
}

// 3. κ²°μ •λœ ν‚€λ‘œ PDF νŒŒμΌμ„ S3에 μ—…λ‘œλ“œ
amazonS3Manager.uploadFileWithTitle(pdfKeyToUse, pdfBytes, "application/pdf", fileTitle);
amazonS3Manager.uploadFileWithTitle(pdfKeyToUse, pdfBytes, "application/pdf", fileTitle + ".pdf");
log.info("PDF μ—…λ‘œλ“œ/κ°±μ‹  성곡. Key: {}", pdfKeyToUse);

// 4. μ‚¬μš©λœ PDF의 keyλ₯Ό λ°˜ν™˜
Expand All @@ -65,7 +65,7 @@ public String createOrUpdateWord(String markdownText, String featurePath, String

// 3. κ²°μ •λœ ν‚€λ‘œ Word νŒŒμΌμ„ S3에 μ—…λ‘œλ“œ
String contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
amazonS3Manager.uploadFileWithTitle(wordKeyToUse, wordBytes, contentType, fileTitle);
amazonS3Manager.uploadFileWithTitle(wordKeyToUse, wordBytes, contentType, fileTitle + ".docx");
log.info("Word 파일 μ—…λ‘œλ“œ/κ°±μ‹  성곡. Key: {}", wordKeyToUse);

// 4. μ‚¬μš©λœ Word의 keyλ₯Ό λ°˜ν™˜
Expand Down
3 changes: 2 additions & 1 deletion src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,5 @@ instagram:
id: "dummy-client-id"
secret: "dummy-client-secret"
redirect:
uri: "dummy-redirect-uri"
uri-onboarding: "dummy-redirect-uri"
uri-workspace: "dummy-redirect-uri"