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
10 changes: 5 additions & 5 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}-backend
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push:
Expand Down Expand Up @@ -64,8 +64,8 @@ jobs:
file: ./Dockerfile
push: true
tags: |
${{ env.REGISTRY }}/${{ steps.string.outputs.IMAGE_NAME_LOWER }}:${{ github.ref_name }}
${{ env.REGISTRY }}/${{ steps.string.outputs.IMAGE_NAME_LOWER }}:latest
${{ env.REGISTRY }}/itzeep/${{ steps.string.outputs.IMAGE_NAME_LOWER }}:${{ github.ref_name }}
${{ env.REGISTRY }}/itzeep/${{ steps.string.outputs.IMAGE_NAME_LOWER }}:latest

deploy:
needs: build-and-push
Expand Down Expand Up @@ -98,7 +98,7 @@ jobs:
echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin

# Pull new image
docker pull ${{ env.REGISTRY }}/${{ steps.string.outputs.IMAGE_NAME_LOWER }}:${{ github.ref_name }}
docker pull ghcr.io/itzeep/backend:latest

# Navigate to project directory
cd $DATA_PATH
Expand All @@ -110,7 +110,7 @@ jobs:
fi

# Update docker-compose.yml to use GHCR image
sed -i "s|build:.*|image: ${{ env.REGISTRY }}/${{ steps.string.outputs.IMAGE_NAME_LOWER }}:${{ github.ref_name }}|g" docker-compose.yml
sed -i "s|build:.*|image: ${{ secrets.IMAGE_NAME}}|g" docker-compose.yml
sed -i "s|context:.*||g" docker-compose.yml
sed -i "s|dockerfile:.*||g" docker-compose.yml

Expand Down
8 changes: 8 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ RUN cd webapps && \
chown -R tomcat:tomcat ROOT && \
chmod -R 755 ROOT

# Copy Firebase service account file to the correct location
# Firebase Admin SDK looks for this file in the classpath
COPY config-submodule/firebase-service-account.json /usr/local/tomcat/webapps/ROOT/WEB-INF/classes/firebase-service-account.json

# Set proper permissions for Firebase service account file
RUN chown tomcat:tomcat /usr/local/tomcat/webapps/ROOT/WEB-INF/classes/firebase-service-account.json && \
chmod 644 /usr/local/tomcat/webapps/ROOT/WEB-INF/classes/firebase-service-account.json

# Create logs directory and set proper permissions
RUN mkdir -p /usr/local/tomcat/logs && \
chmod 777 /usr/local/tomcat/logs && \
Expand Down
2 changes: 1 addition & 1 deletion config-submodule
Original file line number Diff line number Diff line change
Expand Up @@ -2911,98 +2911,100 @@ public Map<String, Object> acceptFinalContract(
return Map.of("accepted", isAccepted);
}

private void broadcastPresence(Long contractChatId) {
ContractChat c = contractChatMapper.findByContractChatId(contractChatId);
if (c == null) return;

boolean ownerIn = isUserInContractChatRoom(c.getOwnerId(), contractChatId);
boolean buyerIn = isUserInContractChatRoom(c.getBuyerId(), contractChatId);
boolean both = ownerIn && buyerIn;

Map<String, Object> payload =
Map.of(
"type", "PRESENCE",
"ownerInContractRoom", ownerIn,
"buyerInContractRoom", buyerIn,
"bothInRoom", both,
"canChat", both,
"ownerId", c.getOwnerId(),
"buyerId", c.getBuyerId());
messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, payload);
}

@Override
public void requestFinalContract(Long contractChatId, Long ownerId) {
ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
if (contractChat == null) {
throw new EntityNotFoundException("계약 채팅방을 찾을 수 없습니다: " + contractChatId);
}

if (!ownerId.equals(contractChat.getOwnerId())) {
throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED);
}

Optional<FinalSpecialContractDocument> finalContractOpt =
specialContractMongoRepository.findFinalContractByContractChatId(contractChatId);

if (finalContractOpt.isEmpty()) {
throw new IllegalArgumentException("최종 특약서가 생성되지 않았습니다.");
}

AiMessageBtn(contractChatId, "임대인이 최종 계약서 확인을 요청하였습니다");

String key = "final-contract:request:" + contractChatId;
String existingValue = stringRedisTemplate.opsForValue().get(key);
if (existingValue != null) {
throw new BusinessException(
ChatErrorCode.CONTRACT_END_REQUEST_ALREADY_EXISTS, "이미 확정 요청이 진행 중입니다.");
}
String value = ownerId.toString();
stringRedisTemplate.opsForValue().set(key, value);
}

@Override
public Map<String, Object> acceptFinalContract(
Long contractChatId, Long buyerId, Boolean isAccepted) {
if (!isUserInContractChat(contractChatId, buyerId)) {
throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED);
}

ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
if (contractChat == null) {
throw new EntityNotFoundException("계약 채팅방을 찾을 수 없습니다: " + contractChatId);
}

Long ownerId = contractChat.getOwnerId();

if (!buyerId.equals(contractChat.getBuyerId())) {
throw new BusinessException(
ChatErrorCode.CHAT_ROOM_ACCESS_DENIED, "임차인만 확정 수락을 할 수 있습니다.");
}

String redisKey = "final-contract:request:" + contractChatId;
String storedOwnerId = stringRedisTemplate.opsForValue().get(redisKey);

if (storedOwnerId == null) {
throw new BusinessException(
ChatErrorCode.CONTRACT_END_REQUEST_NOT_FOUND, "확정 요청이 존재하지 않습니다.");
}

if (!storedOwnerId.equals(ownerId.toString())) {
throw new BusinessException(
ChatErrorCode.CONTRACT_END_REQUEST_INVALID, "확정 요청 정보가 유효하지 않습니다.");
}

stringRedisTemplate.delete(redisKey);

if (isAccepted) {
contractMongoRepository.clearSpecialContracts(contractChatId);
contractMongoRepository.saveSpecialContract(contractChatId);
AiMessage(contractChatId, "임차인이 최종 계약서를 수락했습니다! 계약서 서명하러 갈께요!");
} else {
AiMessage(contractChatId, "임차인이 최종 계약서를 거절했습니다. 추가 협상이 필요합니다.");
}

return Map.of("accepted", isAccepted);
}
// private void broadcastPresence(Long contractChatId) {
// ContractChat c = contractChatMapper.findByContractChatId(contractChatId);
// if (c == null) return;
//
// boolean ownerIn = isUserInContractChatRoom(c.getOwnerId(), contractChatId);
// boolean buyerIn = isUserInContractChatRoom(c.getBuyerId(), contractChatId);
// boolean both = ownerIn && buyerIn;
//
// Map<String, Object> payload =
// Map.of(
// "type", "PRESENCE",
// "ownerInContractRoom", ownerIn,
// "buyerInContractRoom", buyerIn,
// "bothInRoom", both,
// "canChat", both,
// "ownerId", c.getOwnerId(),
// "buyerId", c.getBuyerId());
// messagingTemplate.convertAndSend("/topic/contract-chat/" + contractChatId, payload);
// }

// @Override
// public void requestFinalContract(Long contractChatId, Long ownerId) {
// ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
// if (contractChat == null) {
// throw new EntityNotFoundException("계약 채팅방을 찾을 수 없습니다: " + contractChatId);
// }
//
// if (!ownerId.equals(contractChat.getOwnerId())) {
// throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED);
// }
//
// Optional<FinalSpecialContractDocument> finalContractOpt =
//
// specialContractMongoRepository.findFinalContractByContractChatId(contractChatId);
//
// if (finalContractOpt.isEmpty()) {
// throw new IllegalArgumentException("최종 특약서가 생성되지 않았습니다.");
// }
//
// AiMessageBtn(contractChatId, "임대인이 최종 계약서 확인을 요청하였습니다");
//
// String key = "final-contract:request:" + contractChatId;
// String existingValue = stringRedisTemplate.opsForValue().get(key);
// if (existingValue != null) {
// throw new BusinessException(
// ChatErrorCode.CONTRACT_END_REQUEST_ALREADY_EXISTS, "이미 확정 요청이 진행
// 중입니다.");
// }
// String value = ownerId.toString();
// stringRedisTemplate.opsForValue().set(key, value);
// }

// @Override
// public Map<String, Object> acceptFinalContract(
// Long contractChatId, Long buyerId, Boolean isAccepted) {
// if (!isUserInContractChat(contractChatId, buyerId)) {
// throw new BusinessException(ChatErrorCode.CHAT_ROOM_ACCESS_DENIED);
// }
//
// ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
// if (contractChat == null) {
// throw new EntityNotFoundException("계약 채팅방을 찾을 수 없습니다: " + contractChatId);
// }
//
// Long ownerId = contractChat.getOwnerId();
//
// if (!buyerId.equals(contractChat.getBuyerId())) {
// throw new BusinessException(
// ChatErrorCode.CHAT_ROOM_ACCESS_DENIED, "임차인만 확정 수락을 할 수 있습니다.");
// }
//
// String redisKey = "final-contract:request:" + contractChatId;
// String storedOwnerId = stringRedisTemplate.opsForValue().get(redisKey);
//
// if (storedOwnerId == null) {
// throw new BusinessException(
// ChatErrorCode.CONTRACT_END_REQUEST_NOT_FOUND, "확정 요청이 존재하지 않습니다.");
// }
//
// if (!storedOwnerId.equals(ownerId.toString())) {
// throw new BusinessException(
// ChatErrorCode.CONTRACT_END_REQUEST_INVALID, "확정 요청 정보가 유효하지 않습니다.");
// }
//
// stringRedisTemplate.delete(redisKey);
//
// if (isAccepted) {
// contractMongoRepository.clearSpecialContracts(contractChatId);
// contractMongoRepository.saveSpecialContract(contractChatId);
// AiMessage(contractChatId, "임차인이 최종 계약서를 수락했습니다! 계약서 서명하러 갈께요!");
// } else {
// AiMessage(contractChatId, "임차인이 최종 계약서를 거절했습니다. 추가 협상이 필요합니다.");
// }
//
// return Map.of("accepted", isAccepted);
// }
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package org.scoula.domain.contract.controller;

import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletResponse;

import org.scoula.domain.chat.dto.FinalContractDeletionResponseDto;
import org.scoula.domain.contract.dto.*;
import org.scoula.global.auth.dto.CustomUserDetails;
Expand All @@ -11,6 +14,8 @@
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
Expand Down Expand Up @@ -128,4 +133,74 @@ ResponseEntity<ApiResponse<Map<String, Object>>> acceptFinalContract(
@PathVariable Long contractChatId,
@RequestBody FinalContractDeletionResponseDto responseDto,
Authentication authentication);

// =================

// 내보내기
@ApiOperation(
value = "[내보내기] 0 계약서 내보내기 시작",
notes = "계약서 내보내기 프로세스를 시작합니다. AI 서버에서 초기 PDF를 생성합니다.")
ResponseEntity<byte[]> startContractExport(
@PathVariable Long contractChatId,
@AuthenticationPrincipal CustomUserDetails userDetails);

@ApiOperation(value = "[내보내기] 사용자 역할 확인", notes = "계약서에서 사용자가 owner인지 buyer인지 확인")
ResponseEntity<ApiResponse<String>> getUserRole(
@PathVariable Long contractChatId,
@AuthenticationPrincipal CustomUserDetails userDetails);

@ApiOperation(value = "[내보내기] 1 최종 계약서 PDF로 만들기 -> AI", notes = "최종 계약서에 들어갈 항목들로 최종 계약서 만들기 ")
ResponseEntity<ApiResponse<MultipartFile>> finalContractPDF(
@PathVariable Long contractChatId,
@AuthenticationPrincipal CustomUserDetails userDetails);

@ApiOperation(value = "[내보내기] 2 받아온 전자서명 파일 암호화 후 S3에 저장", notes = "전자서명 png를 s3에 저장합니다.")
ResponseEntity<ApiResponse<Boolean>> saveSignature(
@PathVariable Long contractChatId,
@AuthenticationPrincipal CustomUserDetails userDetails,
@RequestPart("dto") MultipartFile dtoFile, // JSON 파트
@RequestPart("imgFiles") List<MultipartFile> imgFiles)
throws Exception;

@ApiOperation(value = "[내보내기] 3 최종 계약서 PDF S3에 저장", notes = "사용자에게 암호를 받아 암호화 후 S3에 저장하기")
ResponseEntity<ApiResponse<byte[]>> saveFinalContract(
@PathVariable Long contractChatId,
@AuthenticationPrincipal CustomUserDetails userDetails,
@RequestBody ContractPasswordDTO dto);

@ApiOperation(value = "[내보내기] 4 최종 계약서 PDF를 보여줍니다.", notes = "최종 계약서 PDF를 보여줍니다.")
ResponseEntity<ApiResponse<byte[]>> selectContractPDF(
@PathVariable Long contractChatId,
@AuthenticationPrincipal CustomUserDetails userDetails,
@RequestBody ContractPasswordDTO dto)
throws Exception;

// @ApiOperation(value = "최종 계약서 PDF 파일 받아와서 암호화 후 S3에 저장", notes = "최종 계약서 PDF를 암호화하여 S3에
// 저장합니다.")
// ResponseEntity<ApiResponse<Void>> saveContractPDF(
// @PathVariable Long contractChatId,
// @AuthenticationPrincipal CustomUserDetails userDetails,
// @RequestBody FinalContractDTO dto);
//
// @ApiOperation(value = "전자서명 다운로드", notes = "전자서명을 다운로드해서 복호화해서 프론트에 전송합니다.")
// ResponseEntity<ApiResponse<Void>> selectSignaturePDF(
// @PathVariable Long contractChatId,
// @AuthenticationPrincipal CustomUserDetails userDetails,
// HttpServletResponse response);

// 패스베리어블을 뭘로 받아올지 얘기해보기 : contract_id
@ApiOperation(value = "[내보내기] 5 계약서 PDF 파일 다운로드", notes = "계약서 PDF를 S3에서 꺼내 보내준다")
ResponseEntity<ApiResponse<Void>> selectContractPDF(
@PathVariable Long contractChatId,
@AuthenticationPrincipal CustomUserDetails userDetails,
HttpServletResponse response,
@RequestBody FindContractDTO dto)
throws Exception;

@ApiOperation(value = "[내보내기] 6 최종 계약서 PDF를 이메일로 전송", notes = "최종 계약서 PDF를 이메일로 전송합니다.")
ResponseEntity<ApiResponse<Void>> sendContractPDF(
@PathVariable Long contractChatId,
@AuthenticationPrincipal CustomUserDetails userDetails,
@RequestBody FindContractDTO dto)
throws Exception;
}
Loading
Loading