Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 67ec6d2103 | |||
| b024f8e86f | |||
| fd16300553 | |||
| 815c329e3c | |||
| 7fdd70dbc7 | |||
| 3dd9edc849 | |||
| 6cff96f40a | |||
| a97609d533 |
@@ -0,0 +1,182 @@
|
|||||||
|
pipeline {
|
||||||
|
agent { label 'djb-vm' }
|
||||||
|
|
||||||
|
parameters {
|
||||||
|
string(name: 'BRANCH', defaultValue: 'master', description: 'Branch to deploy')
|
||||||
|
}
|
||||||
|
|
||||||
|
options {
|
||||||
|
timestamps()
|
||||||
|
disableConcurrentBuilds()
|
||||||
|
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
JAVA_HOME = '/apps/opts/jdk8'
|
||||||
|
JAVA_HOME_TOMCAT = '/apps/opts/jdk17'
|
||||||
|
GRADLE_HOME = '/apps/opts/gradle-8.7'
|
||||||
|
GRADLE_USER_HOME = '/apps/opts/gradle-home'
|
||||||
|
NODE_HOME = '/apps/opts/node-v24'
|
||||||
|
PATH = "/apps/opts/jdk8/bin:/apps/opts/gradle-8.7/bin:/apps/opts/node-v24/bin:/apps/opts/bin:${env.PATH}"
|
||||||
|
GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new'
|
||||||
|
CATALINA_BASE = '/prod/eapim/devportal'
|
||||||
|
CATALINA_HOME = '/prod/eapim/apache-tomcat-9.0.116'
|
||||||
|
CATALINA_PID = '/prod/eapim/devportal/temp/catalina.pid'
|
||||||
|
DEPLOY_HTTP_PORT = '39130'
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Checkout') {
|
||||||
|
steps {
|
||||||
|
checkout([
|
||||||
|
$class: 'GitSCM',
|
||||||
|
branches: [[name: "*/${params.BRANCH}"]],
|
||||||
|
userRemoteConfigs: [[url: 'ssh://git@172.30.1.50:2222/djb-eapim/eapim-portal.git']]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Checkout dependencies') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
set -eu
|
||||||
|
cd "$WORKSPACE/.."
|
||||||
|
|
||||||
|
if [ ! -d elink-portal-common/.git ]; then
|
||||||
|
git clone --depth=1 --branch master \
|
||||||
|
ssh://git@172.30.1.50:2222/djb-eapim/elink-portal-common.git \
|
||||||
|
elink-portal-common
|
||||||
|
else
|
||||||
|
git -C elink-portal-common fetch --depth=1 origin master
|
||||||
|
git -C elink-portal-common reset --hard origin/master
|
||||||
|
git -C elink-portal-common clean -fdx
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p eapim-online
|
||||||
|
if [ ! -d eapim-online/elink-online-core-jpa/.git ]; then
|
||||||
|
git clone --depth=1 --branch master \
|
||||||
|
ssh://git@172.30.1.50:2222/djb-eapim/elink-online-core-jpa.git \
|
||||||
|
eapim-online/elink-online-core-jpa
|
||||||
|
else
|
||||||
|
git -C eapim-online/elink-online-core-jpa fetch --depth=1 origin master
|
||||||
|
git -C eapim-online/elink-online-core-jpa reset --hard origin/master
|
||||||
|
git -C eapim-online/elink-online-core-jpa clean -fdx
|
||||||
|
fi
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Verify toolchain') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
set -eu
|
||||||
|
java -version
|
||||||
|
gradle --version
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Build WAR') {
|
||||||
|
steps {
|
||||||
|
sh 'gradle clean build -x test --no-daemon'
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
success {
|
||||||
|
sh '''
|
||||||
|
set -eu
|
||||||
|
cd build/libs
|
||||||
|
sha1sum eapim-portal.war eapim-portal-boot.war > SHA1SUMS
|
||||||
|
sha256sum eapim-portal.war eapim-portal-boot.war > SHA256SUMS
|
||||||
|
'''
|
||||||
|
archiveArtifacts artifacts: 'build/libs/eapim-portal.war,build/libs/eapim-portal-boot.war,build/libs/SHA1SUMS,build/libs/SHA256SUMS', fingerprint: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Stop Tomcat') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
set +e
|
||||||
|
systemctl --user stop eapim-portal 2>/dev/null
|
||||||
|
|
||||||
|
if [ -f "$CATALINA_PID" ] && kill -0 "$(cat "$CATALINA_PID")" 2>/dev/null; then
|
||||||
|
JAVA_HOME="$JAVA_HOME_TOMCAT" "$CATALINA_HOME/bin/shutdown.sh" 30 -force || true
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
[ -f "$CATALINA_PID" ] && kill -0 "$(cat "$CATALINA_PID")" 2>/dev/null || break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
rm -f "$CATALINA_PID"
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Deploy ROOT.war') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
set -eu
|
||||||
|
DEPLOY_DIR="$CATALINA_BASE/webapps"
|
||||||
|
rm -rf "$DEPLOY_DIR/ROOT" "$DEPLOY_DIR/ROOT.war"
|
||||||
|
cp build/libs/eapim-portal.war "$DEPLOY_DIR/ROOT.war"
|
||||||
|
ls -la "$DEPLOY_DIR"
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Start Tomcat and readiness') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
set -eu
|
||||||
|
LOG="$CATALINA_BASE/logs/catalina.$(date +%Y-%m-%d).log"
|
||||||
|
BEFORE=0
|
||||||
|
[ -f "$LOG" ] && BEFORE=$(stat -c %s "$LOG")
|
||||||
|
|
||||||
|
systemctl --user start eapim-portal
|
||||||
|
|
||||||
|
DEADLINE=$(($(date +%s) + 120))
|
||||||
|
LIVE_OK=0
|
||||||
|
STATUS=000
|
||||||
|
|
||||||
|
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
||||||
|
if [ "$LIVE_OK" = "0" ]; then
|
||||||
|
LIVE=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 3 \
|
||||||
|
"http://localhost:$DEPLOY_HTTP_PORT/health" 2>/dev/null || echo "000")
|
||||||
|
if [ "$LIVE" = "200" ]; then
|
||||||
|
echo "Liveness OK (/health)"
|
||||||
|
LIVE_OK=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$LIVE_OK" = "1" ]; then
|
||||||
|
STATUS=$(curl -sS -o /tmp/eapim-ready-body.json -w '%{http_code}' --max-time 3 \
|
||||||
|
"http://localhost:$DEPLOY_HTTP_PORT/health/ready" 2>/dev/null || echo "000")
|
||||||
|
if [ "$STATUS" = "200" ]; then
|
||||||
|
echo "Readiness OK (/health/ready)"
|
||||||
|
cat /tmp/eapim-ready-body.json
|
||||||
|
echo
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | grep -qE "Application listener .* Exception|SEVERE.*startup.Catalina"; then
|
||||||
|
echo "Startup error detected in log"
|
||||||
|
tail -c +$((BEFORE+1)) "$LOG" | tail -80
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$STATUS" != "200" ]; then
|
||||||
|
echo "Readiness probe failed within 120s, last HTTP status: $STATUS"
|
||||||
|
[ -f /tmp/eapim-ready-body.json ] && cat /tmp/eapim-ready-body.json && echo
|
||||||
|
[ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | tail -100
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "--- key boot log lines ---"
|
||||||
|
tail -c +$((BEFORE+1)) "$LOG" | grep -E "Server startup|Deployment of web" | head -10 || true
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
pipeline {
|
||||||
|
agent { label 'djb-vm' }
|
||||||
|
|
||||||
|
triggers {
|
||||||
|
pollSCM('H/10 * * * *')
|
||||||
|
}
|
||||||
|
|
||||||
|
options {
|
||||||
|
timestamps()
|
||||||
|
disableConcurrentBuilds()
|
||||||
|
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
JAVA_HOME = '/apps/opts/jdk8'
|
||||||
|
JAVA_HOME_TOMCAT = '/apps/opts/jdk17'
|
||||||
|
GRADLE_HOME = '/apps/opts/gradle-8.7'
|
||||||
|
GRADLE_USER_HOME = '/apps/opts/gradle-home'
|
||||||
|
NODE_HOME = '/apps/opts/node-v24'
|
||||||
|
PATH = "/apps/opts/jdk8/bin:/apps/opts/gradle-8.7/bin:/apps/opts/node-v24/bin:/apps/opts/bin:${env.PATH}"
|
||||||
|
GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new'
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Checkout') {
|
||||||
|
steps {
|
||||||
|
checkout scm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Checkout dependencies') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
set -eu
|
||||||
|
cd "$WORKSPACE/.."
|
||||||
|
|
||||||
|
if [ ! -d elink-portal-common/.git ]; then
|
||||||
|
git clone --depth=1 --branch master \
|
||||||
|
ssh://git@172.30.1.50:2222/djb-eapim/elink-portal-common.git \
|
||||||
|
elink-portal-common
|
||||||
|
else
|
||||||
|
git -C elink-portal-common fetch --depth=1 origin master
|
||||||
|
git -C elink-portal-common reset --hard origin/master
|
||||||
|
git -C elink-portal-common clean -fdx
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p eapim-online
|
||||||
|
if [ ! -d eapim-online/elink-online-core-jpa/.git ]; then
|
||||||
|
git clone --depth=1 --branch master \
|
||||||
|
ssh://git@172.30.1.50:2222/djb-eapim/elink-online-core-jpa.git \
|
||||||
|
eapim-online/elink-online-core-jpa
|
||||||
|
else
|
||||||
|
git -C eapim-online/elink-online-core-jpa fetch --depth=1 origin master
|
||||||
|
git -C eapim-online/elink-online-core-jpa reset --hard origin/master
|
||||||
|
git -C eapim-online/elink-online-core-jpa clean -fdx
|
||||||
|
fi
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Verify toolchain') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
set -eu
|
||||||
|
java -version
|
||||||
|
gradle --version
|
||||||
|
node --version || true
|
||||||
|
npm --version || true
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Test') {
|
||||||
|
steps {
|
||||||
|
sh 'gradle clean test --no-daemon'
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
junit allowEmptyResults: true, testResults: 'build/test-results/test/*.xml'
|
||||||
|
archiveArtifacts allowEmptyArchive: true, artifacts: 'build/reports/tests/test/**,build/test-results/test/**'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Build WAR') {
|
||||||
|
steps {
|
||||||
|
sh 'gradle build -x test --no-daemon'
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
success {
|
||||||
|
sh '''
|
||||||
|
set -eu
|
||||||
|
cd build/libs
|
||||||
|
sha1sum eapim-portal.war eapim-portal-boot.war > SHA1SUMS
|
||||||
|
sha256sum eapim-portal.war eapim-portal-boot.war > SHA256SUMS
|
||||||
|
'''
|
||||||
|
archiveArtifacts artifacts: 'build/libs/eapim-portal.war,build/libs/eapim-portal-boot.war,build/libs/SHA1SUMS,build/libs/SHA256SUMS', fingerprint: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-15
@@ -6,11 +6,7 @@ import com.eactive.apim.portal.apps.community.qna.service.InquiryFacade;
|
|||||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||||
import com.eactive.apim.portal.common.util.UserTypeUtil;
|
import com.eactive.apim.portal.common.util.UserTypeUtil;
|
||||||
import com.eactive.apim.portal.djb.community.qna.comment.service.InquiryCommentFacade;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
@@ -33,12 +29,9 @@ public class InquiryController {
|
|||||||
public static final String APPS_COMMUNITY_MAIN_INQUIRY_FORM = "apps/community/mainInquiryForm";
|
public static final String APPS_COMMUNITY_MAIN_INQUIRY_FORM = "apps/community/mainInquiryForm";
|
||||||
public static final String REDIRECT_INQUIRY = "redirect:/inquiry";
|
public static final String REDIRECT_INQUIRY = "redirect:/inquiry";
|
||||||
private final InquiryFacade inquiryFacade;
|
private final InquiryFacade inquiryFacade;
|
||||||
private final InquiryCommentFacade inquiryCommentFacade;
|
|
||||||
|
|
||||||
public InquiryController(InquiryFacade inquiryFacade,
|
public InquiryController(InquiryFacade inquiryFacade) {
|
||||||
InquiryCommentFacade inquiryCommentFacade) {
|
|
||||||
this.inquiryFacade = inquiryFacade;
|
this.inquiryFacade = inquiryFacade;
|
||||||
this.inquiryCommentFacade = inquiryCommentFacade;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,13 +42,8 @@ public class InquiryController {
|
|||||||
Model model) {
|
Model model) {
|
||||||
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
|
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
|
||||||
Page<InquiryDTO> inquiries = inquiryFacade.getInquiries(search, pageable, user);
|
Page<InquiryDTO> inquiries = inquiryFacade.getInquiries(search, pageable, user);
|
||||||
List<String> ids = inquiries.getContent().stream()
|
|
||||||
.map(InquiryDTO::getId)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
Map<String, Long> commentCounts = inquiryCommentFacade.countActiveByInquiryIds(ids);
|
|
||||||
model.addAttribute("inquiries", inquiries.getContent());
|
model.addAttribute("inquiries", inquiries.getContent());
|
||||||
model.addAttribute("page", inquiries);
|
model.addAttribute("page", inquiries);
|
||||||
model.addAttribute("commentCounts", commentCounts);
|
|
||||||
return "apps/community/mainInquiryList";
|
return "apps/community/mainInquiryList";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,9 +57,8 @@ public class InquiryController {
|
|||||||
return REDIRECT_INQUIRY;
|
return REDIRECT_INQUIRY;
|
||||||
}
|
}
|
||||||
|
|
||||||
InquiryDTO inquiry = inquiryFacade.getAccessibleInquiry(SecurityUtil.getPortalAuthenticatedUser(), id);
|
InquiryDTO inquiry = inquiryFacade.getMyInquiry(SecurityUtil.getPortalAuthenticatedUser(), id);
|
||||||
model.addAttribute("inquiry", inquiry);
|
model.addAttribute("inquiry", inquiry);
|
||||||
model.addAttribute("commentWritable", !"CLOSED".equals(inquiry.getInquiryStatus()));
|
|
||||||
return "apps/community/mainInquiryDetail";
|
return "apps/community/mainInquiryDetail";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import org.hibernate.validator.constraints.NotEmpty;
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import javax.validation.constraints.Pattern;
|
import javax.validation.constraints.Pattern;
|
||||||
import javax.validation.constraints.Size;
|
import javax.validation.constraints.Size;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@@ -47,6 +50,8 @@ public class InquiryDTO {
|
|||||||
|
|
||||||
private LocalDateTime lastModifiedDate;
|
private LocalDateTime lastModifiedDate;
|
||||||
|
|
||||||
|
private List<MultipartFile> files;
|
||||||
|
|
||||||
private String attachFile;
|
private String attachFile;
|
||||||
|
|
||||||
private String maskedInquirerName;
|
private String maskedInquirerName;
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ public interface InquiryFacade {
|
|||||||
|
|
||||||
InquiryDTO getMyInquiry(PortalAuthenticatedUser user, String id);
|
InquiryDTO getMyInquiry(PortalAuthenticatedUser user, String id);
|
||||||
|
|
||||||
InquiryDTO getAccessibleInquiry(PortalAuthenticatedUser user, String id);
|
|
||||||
|
|
||||||
void createInquiry(InquiryDTO inquiryDTO) throws IOException;
|
void createInquiry(InquiryDTO inquiryDTO) throws IOException;
|
||||||
|
|
||||||
void updateUserInquiry(PortalAuthenticatedUser user, String id, InquiryDTO inquiryDTO) throws IOException;
|
void updateUserInquiry(PortalAuthenticatedUser user, String id, InquiryDTO inquiryDTO) throws IOException;
|
||||||
|
|||||||
+43
-17
@@ -6,10 +6,12 @@ import com.eactive.apim.portal.apps.community.qna.mapper.InquiryMapper;
|
|||||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||||
import com.eactive.apim.portal.common.util.StringMaskingUtil;
|
import com.eactive.apim.portal.common.util.StringMaskingUtil;
|
||||||
import com.eactive.apim.portal.djb.community.qna.comment.service.InquiryAdminNotifier;
|
import com.eactive.apim.portal.common.util.UserTypeUtil;
|
||||||
|
import com.eactive.apim.portal.file.entity.FileInfo;
|
||||||
|
import com.eactive.apim.portal.file.service.FileService;
|
||||||
|
import com.eactive.apim.portal.file.service.FileTypeContext;
|
||||||
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
|
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
|
||||||
import com.eactive.apim.portal.qna.entity.Inquiry;
|
import com.eactive.apim.portal.qna.entity.Inquiry;
|
||||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
@@ -18,15 +20,14 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class InquiryFacadeImpl implements InquiryFacade {
|
public class InquiryFacadeImpl implements InquiryFacade {
|
||||||
|
|
||||||
|
private final FileService fileService;
|
||||||
private final InquiryService inquiryService;
|
private final InquiryService inquiryService;
|
||||||
private final InquiryMapper inquiryMapper;
|
private final InquiryMapper inquiryMapper;
|
||||||
private final InquiryAdminNotifier inquiryAdminNotifier;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Page<InquiryDTO> getInquiries(InquirySearch search, Pageable pageable, PortalAuthenticatedUser user) {
|
public Page<InquiryDTO> getInquiries(InquirySearch search, Pageable pageable, PortalAuthenticatedUser user) {
|
||||||
@@ -78,30 +79,55 @@ public class InquiryFacadeImpl implements InquiryFacade {
|
|||||||
return inquiryMapper.map(inquiry);
|
return inquiryMapper.map(inquiry);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public InquiryDTO getAccessibleInquiry(PortalAuthenticatedUser user, String id) {
|
|
||||||
Inquiry inquiry = inquiryService.getAccessibleInquiry(user, id);
|
|
||||||
return inquiryMapper.map(inquiry);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void createInquiry(InquiryDTO inquiryDTO) throws IOException {
|
public void createInquiry(InquiryDTO inquiryDTO) throws IOException {
|
||||||
PortalAuthenticatedUser current = SecurityUtil.getPortalAuthenticatedUser();
|
|
||||||
|
FileInfo file = null;
|
||||||
|
if (!inquiryDTO.getFiles().isEmpty()) {
|
||||||
|
try {
|
||||||
|
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
|
||||||
|
FileService.setInternalUserContext(UserTypeUtil.isInternalUser(user));
|
||||||
|
|
||||||
|
FileTypeContext.setFileType("qna");
|
||||||
|
file = fileService.createFile(inquiryDTO.getFiles());
|
||||||
|
} finally {
|
||||||
|
FileService.clearInternalUserContext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Inquiry inquiry = inquiryMapper.toEntity(inquiryDTO);
|
Inquiry inquiry = inquiryMapper.toEntity(inquiryDTO);
|
||||||
inquiry.setInquirer(current);
|
if (file != null) {
|
||||||
|
inquiry.setAttachFile(file.getFileId());
|
||||||
|
}
|
||||||
|
inquiry.setInquirer(SecurityUtil.getPortalAuthenticatedUser());
|
||||||
inquiryService.createInquiry(inquiry);
|
inquiryService.createInquiry(inquiry);
|
||||||
|
|
||||||
Map<String, Object> params = new HashMap<>();
|
HashMap<String, Object> params = new HashMap<>();
|
||||||
params.put("inquiryId", inquiry.getId());
|
params.put("inquiryId", inquiry.getId());
|
||||||
params.put("inquirySubject", inquiry.getInquirySubject());
|
params.put("subject", inquiry.getInquirySubject());
|
||||||
params.put("writerName", current.getUserName());
|
|
||||||
inquiryAdminNotifier.notifyPortalAdmins(MessageCode.INQUIRY_CREATED, params);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateUserInquiry(PortalAuthenticatedUser user, String id, InquiryDTO inquiryDTO) throws IOException {
|
public void updateUserInquiry(PortalAuthenticatedUser user, String id, InquiryDTO inquiryDTO) throws IOException {
|
||||||
Inquiry existingInquiry = inquiryService.getMyInquiry(user, id);
|
Inquiry existingInquiry = inquiryService.getMyInquiry(user, id);
|
||||||
inquiryDTO.setAttachFile(existingInquiry.getAttachFile());
|
|
||||||
|
if (!inquiryDTO.getFiles().isEmpty() && !inquiryDTO.getFiles().get(0).isEmpty()) {
|
||||||
|
try {
|
||||||
|
FileService.setInternalUserContext(UserTypeUtil.isInternalUser(user));
|
||||||
|
|
||||||
|
FileTypeContext.setFileType("qna");
|
||||||
|
FileInfo file = fileService.createFile(inquiryDTO.getFiles());
|
||||||
|
if(file != null) {
|
||||||
|
inquiryDTO.setAttachFile(file.getFileId());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
FileService.clearInternalUserContext();
|
||||||
|
}
|
||||||
|
} else if (existingInquiry.getAttachFile() != null && inquiryDTO.getAttachFile() == null) {
|
||||||
|
fileService.deleteFile(inquiryDTO.getAttachFile());
|
||||||
|
inquiryDTO.setAttachFile(null);
|
||||||
|
}
|
||||||
|
|
||||||
Inquiry inquiry = inquiryMapper.toEntity(inquiryDTO);
|
Inquiry inquiry = inquiryMapper.toEntity(inquiryDTO);
|
||||||
inquiry.setInquirer(existingInquiry.getInquirer());
|
inquiry.setInquirer(existingInquiry.getInquirer());
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ package com.eactive.apim.portal.apps.community.qna.service;
|
|||||||
import com.eactive.apim.portal.apps.community.qna.repository.InquiryRepository;
|
import com.eactive.apim.portal.apps.community.qna.repository.InquiryRepository;
|
||||||
import com.eactive.apim.portal.common.exception.NotFoundException;
|
import com.eactive.apim.portal.common.exception.NotFoundException;
|
||||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||||
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
|
||||||
import com.eactive.apim.portal.portaluser.entity.PortalUser;
|
|
||||||
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
|
|
||||||
import com.eactive.apim.portal.qna.entity.Inquiry;
|
import com.eactive.apim.portal.qna.entity.Inquiry;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
@@ -93,36 +90,5 @@ public class InquiryService {
|
|||||||
return inquiryRepository.findByInquirerAndId(user, id).orElseThrow(() -> new NotFoundException("내 질문을 찾을 수 없습니다."));
|
return inquiryRepository.findByInquirerAndId(user, id).orElseThrow(() -> new NotFoundException("내 질문을 찾을 수 없습니다."));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 본인 또는 같은 법인 소속 이용자가 작성한 Q&A를 조회한다.
|
|
||||||
* - ROLE_USER: 본인 글만 접근 가능
|
|
||||||
* - ROLE_CORP_*: 같은 법인 소속 이용자가 작성한 글까지 접근 가능
|
|
||||||
*/
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public Inquiry getAccessibleInquiry(PortalAuthenticatedUser user, String id) {
|
|
||||||
Inquiry inquiry = getInquiry(id);
|
|
||||||
if (!isAccessibleByUser(user, inquiry)) {
|
|
||||||
throw new NotFoundException("문의글을 찾을 수 없습니다. id=" + id);
|
|
||||||
}
|
|
||||||
return inquiry;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isAccessibleByUser(PortalAuthenticatedUser user, Inquiry inquiry) {
|
|
||||||
PortalUser inquirer = inquiry.getInquirer();
|
|
||||||
if (inquirer == null || user == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (user.getId() != null && user.getId().equals(inquirer.getId())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (user.getRoleCode() == PortalUserEnums.RoleCode.ROLE_USER) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
PortalOrg userOrg = user.getPortalOrg();
|
|
||||||
PortalOrg inquirerOrg = inquirer.getPortalOrg();
|
|
||||||
return userOrg != null && inquirerOrg != null
|
|
||||||
&& userOrg.getId() != null
|
|
||||||
&& userOrg.getId().equals(inquirerOrg.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
-73
@@ -1,73 +0,0 @@
|
|||||||
package com.eactive.apim.portal.djb.community.qna.comment.controller;
|
|
||||||
|
|
||||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
|
||||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.comment.dto.InquiryCommentCreateRequest;
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.comment.dto.InquiryCommentDTO;
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.comment.service.InquiryCommentFacade;
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.exception.InquiryClosedException;
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.exception.InquiryCommentNotOwnedException;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.access.annotation.Secured;
|
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
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;
|
|
||||||
|
|
||||||
import javax.validation.Valid;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/djb/inquiry")
|
|
||||||
@Secured("ROLE_INQUIRY")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InquiryCommentController {
|
|
||||||
|
|
||||||
private final InquiryCommentFacade inquiryCommentFacade;
|
|
||||||
|
|
||||||
@GetMapping("/{inquiryId}/comments")
|
|
||||||
public ResponseEntity<List<InquiryCommentDTO>> list(@PathVariable String inquiryId) {
|
|
||||||
PortalAuthenticatedUser current = SecurityUtil.getPortalAuthenticatedUser();
|
|
||||||
return ResponseEntity.ok(inquiryCommentFacade.getComments(inquiryId, current));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/{inquiryId}/comments")
|
|
||||||
public ResponseEntity<InquiryCommentDTO> create(@PathVariable String inquiryId,
|
|
||||||
@Valid @RequestBody InquiryCommentCreateRequest request) {
|
|
||||||
PortalAuthenticatedUser current = SecurityUtil.getPortalAuthenticatedUser();
|
|
||||||
InquiryCommentDTO created = inquiryCommentFacade.createUserComment(inquiryId, request.getContent(), current);
|
|
||||||
return ResponseEntity.ok(created);
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/comments/{commentId}")
|
|
||||||
public ResponseEntity<Void> delete(@PathVariable String commentId) {
|
|
||||||
PortalAuthenticatedUser current = SecurityUtil.getPortalAuthenticatedUser();
|
|
||||||
inquiryCommentFacade.deleteOwnComment(commentId, current);
|
|
||||||
return ResponseEntity.noContent().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExceptionHandler(InquiryClosedException.class)
|
|
||||||
public ResponseEntity<Map<String, String>> handleClosed(InquiryClosedException ex) {
|
|
||||||
return ResponseEntity.status(HttpStatus.CONFLICT).body(errorBody("INQUIRY_CLOSED", ex.getMessage()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExceptionHandler(InquiryCommentNotOwnedException.class)
|
|
||||||
public ResponseEntity<Map<String, String>> handleNotOwned(InquiryCommentNotOwnedException ex) {
|
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorBody("COMMENT_NOT_OWNED", ex.getMessage()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, String> errorBody(String code, String message) {
|
|
||||||
Map<String, String> body = new HashMap<>();
|
|
||||||
body.put("code", code);
|
|
||||||
body.put("message", message);
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
package com.eactive.apim.portal.djb.community.qna.comment.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import org.hibernate.validator.constraints.NotBlank;
|
|
||||||
|
|
||||||
import javax.validation.constraints.Size;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class InquiryCommentCreateRequest {
|
|
||||||
|
|
||||||
@NotBlank(message = "댓글 내용을 입력해주세요.")
|
|
||||||
@Size(max = 2000, message = "댓글은 2000자를 초과할 수 없습니다.")
|
|
||||||
private String content;
|
|
||||||
}
|
|
||||||
-29
@@ -1,29 +0,0 @@
|
|||||||
package com.eactive.apim.portal.djb.community.qna.comment.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class InquiryCommentDTO {
|
|
||||||
|
|
||||||
private String id;
|
|
||||||
|
|
||||||
private String content;
|
|
||||||
|
|
||||||
private String writerId;
|
|
||||||
|
|
||||||
private String writerName;
|
|
||||||
|
|
||||||
private String adminYn;
|
|
||||||
|
|
||||||
private LocalDateTime createdDate;
|
|
||||||
|
|
||||||
private boolean deletable;
|
|
||||||
}
|
|
||||||
-54
@@ -1,54 +0,0 @@
|
|||||||
package com.eactive.apim.portal.djb.community.qna.comment.event;
|
|
||||||
|
|
||||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
|
||||||
import com.eactive.apim.portal.template.service.MessageEventHandler;
|
|
||||||
import com.eactive.apim.portal.template.service.MessageRecipient;
|
|
||||||
import com.eactive.apim.portal.template.service.MessageSendEvent;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class InquiryCommentCreatedAdminEvent implements MessageEventHandler {
|
|
||||||
|
|
||||||
public static final MessageCode KEY = MessageCode.INQUIRY_COMMENT_CREATED;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MessageCode getKey() {
|
|
||||||
return KEY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean allowAdditionalRecipients() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDisplayName() {
|
|
||||||
return "Q&A 댓글 등록 알림";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "<div>사용 가능 변수: </div><br/>"
|
|
||||||
+ "<div> - inquiryId: 문의 ID </div><br/>"
|
|
||||||
+ "<div> - inquirySubject: 문의 제목 </div><br/>"
|
|
||||||
+ "<div> - commentContent: 댓글 본문 </div><br/>"
|
|
||||||
+ "<div> - writerName: 작성자 이름 </div><br/>";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) {
|
|
||||||
Map<String, String> requestParams = new HashMap<>();
|
|
||||||
requestParams.put("inquiryId", toStr(params.get("inquiryId")));
|
|
||||||
requestParams.put("inquirySubject", toStr(params.get("inquirySubject")));
|
|
||||||
requestParams.put("commentContent", toStr(params.get("commentContent")));
|
|
||||||
requestParams.put("writerName", toStr(params.get("writerName")));
|
|
||||||
return new MessageSendEvent(source, KEY, recipient, requestParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String toStr(Object value) {
|
|
||||||
return value == null ? "" : value.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-20
@@ -1,20 +0,0 @@
|
|||||||
package com.eactive.apim.portal.djb.community.qna.comment.repository;
|
|
||||||
|
|
||||||
import com.eactive.apim.portal.qna.entity.InquiryComment;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public interface InquiryCommentRepository extends JpaRepository<InquiryComment, String> {
|
|
||||||
|
|
||||||
List<InquiryComment> findByInquiry_IdAndDelYnOrderByCreatedDateAsc(String inquiryId, String delYn);
|
|
||||||
|
|
||||||
@Query("select c.inquiry.id, count(c) from InquiryComment c"
|
|
||||||
+ " where c.inquiry.id in :inquiryIds and c.delYn = :delYn"
|
|
||||||
+ " group by c.inquiry.id")
|
|
||||||
List<Object[]> countActiveGroupByInquiry(@Param("inquiryIds") Collection<String> inquiryIds,
|
|
||||||
@Param("delYn") String delYn);
|
|
||||||
}
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
package com.eactive.apim.portal.djb.community.qna.comment.repository;
|
|
||||||
|
|
||||||
import com.eactive.apim.portal.user.entity.UserInfo;
|
|
||||||
import com.eactive.eai.data.jpa.BaseRepository;
|
|
||||||
import com.eactive.eai.rms.data.EMSDataSource;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@EMSDataSource
|
|
||||||
public interface UserInfoRepository extends BaseRepository<UserInfo, String> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TSEAIRM02.roleidnfiname 컬럼은 콤마로 구분된 복수 역할을 저장한다.
|
|
||||||
* (예: {@code "admin,portal-admin"}). Oracle native query로 콤마 토큰 매치.
|
|
||||||
*/
|
|
||||||
@Query(value = "SELECT * FROM TSEAIRM02 t"
|
|
||||||
+ " WHERE ',' || t.ROLEIDNFINAME || ',' LIKE '%,' || :role || ',%'",
|
|
||||||
nativeQuery = true)
|
|
||||||
List<UserInfo> findByRoleContaining(@Param("role") String role);
|
|
||||||
}
|
|
||||||
-57
@@ -1,57 +0,0 @@
|
|||||||
package com.eactive.apim.portal.djb.community.qna.comment.service;
|
|
||||||
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.comment.repository.UserInfoRepository;
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.constant.DjbAdminRole;
|
|
||||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
|
||||||
import com.eactive.apim.portal.template.service.MessageHandlerService;
|
|
||||||
import com.eactive.apim.portal.template.service.MessageRecipient;
|
|
||||||
import com.eactive.apim.portal.user.entity.UserInfo;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Q&A 등록/댓글 등록 시 portal-admin 역할(TSEAIRM02)을 가진 관리자 전원에게
|
|
||||||
* 알림 메시지를 발행한다. 발송 실패는 트랜잭션 롤백을 유발하지 않는다.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InquiryAdminNotifier {
|
|
||||||
|
|
||||||
private final UserInfoRepository userInfoRepository;
|
|
||||||
private final MessageHandlerService messageHandlerService;
|
|
||||||
|
|
||||||
public void notifyPortalAdmins(MessageCode code, Map<String, Object> params) {
|
|
||||||
try {
|
|
||||||
List<UserInfo> admins = userInfoRepository.findByRoleContaining(DjbAdminRole.PORTAL_ADMIN);
|
|
||||||
if (admins == null || admins.isEmpty()) {
|
|
||||||
log.warn("portal-admin 역할 관리자가 없습니다 — 알림 미발송 code={}", code.name());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (UserInfo admin : admins) {
|
|
||||||
try {
|
|
||||||
MessageRecipient recipient = toRecipient(admin);
|
|
||||||
messageHandlerService.publishEvent(code, recipient, params);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("portal-admin 개별 알림 발행 실패 — userid={}, code={}",
|
|
||||||
admin.getUserid(), code.name(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("portal-admin 알림 발행 실패 — code={}", code.name(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private MessageRecipient toRecipient(UserInfo admin) {
|
|
||||||
MessageRecipient r = new MessageRecipient();
|
|
||||||
r.setUsername(admin.getUsername());
|
|
||||||
r.setUserId(admin.getEmad());
|
|
||||||
r.setPhone(admin.getCphnno());
|
|
||||||
r.setMessengerId(admin.getUserid());
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
package com.eactive.apim.portal.djb.community.qna.comment.service;
|
|
||||||
|
|
||||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.comment.dto.InquiryCommentDTO;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public interface InquiryCommentFacade {
|
|
||||||
|
|
||||||
List<InquiryCommentDTO> getComments(String inquiryId, PortalAuthenticatedUser current);
|
|
||||||
|
|
||||||
InquiryCommentDTO createUserComment(String inquiryId, String content, PortalAuthenticatedUser current);
|
|
||||||
|
|
||||||
void deleteOwnComment(String commentId, PortalAuthenticatedUser current);
|
|
||||||
|
|
||||||
Map<String, Long> countActiveByInquiryIds(Collection<String> inquiryIds);
|
|
||||||
}
|
|
||||||
-177
@@ -1,177 +0,0 @@
|
|||||||
package com.eactive.apim.portal.djb.community.qna.comment.service;
|
|
||||||
|
|
||||||
import com.eactive.apim.portal.apps.community.qna.service.InquiryService;
|
|
||||||
import com.eactive.apim.portal.common.exception.NotFoundException;
|
|
||||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.comment.dto.InquiryCommentDTO;
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.comment.repository.InquiryCommentRepository;
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.comment.repository.UserInfoRepository;
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.constant.DjbInquiryStatus;
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.support.InquiryCommentPermissionChecker;
|
|
||||||
import com.eactive.apim.portal.portaluser.entity.PortalUser;
|
|
||||||
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
|
|
||||||
import com.eactive.apim.portal.qna.entity.Inquiry;
|
|
||||||
import com.eactive.apim.portal.qna.entity.InquiryComment;
|
|
||||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
|
||||||
import com.eactive.apim.portal.user.entity.UserInfo;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@Transactional
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InquiryCommentFacadeImpl implements InquiryCommentFacade {
|
|
||||||
|
|
||||||
private static final String ADMIN_N = "N";
|
|
||||||
private static final String DEL_N = "N";
|
|
||||||
|
|
||||||
private final InquiryService inquiryService;
|
|
||||||
private final InquiryCommentService commentService;
|
|
||||||
private final InquiryCommentRepository commentRepository;
|
|
||||||
private final InquiryCommentPermissionChecker permissionChecker;
|
|
||||||
private final InquiryAdminNotifier adminNotifier;
|
|
||||||
private final PortalUserRepository portalUserRepository;
|
|
||||||
private final UserInfoRepository userInfoRepository;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public List<InquiryCommentDTO> getComments(String inquiryId, PortalAuthenticatedUser current) {
|
|
||||||
inquiryService.getAccessibleInquiry(current, inquiryId);
|
|
||||||
List<InquiryComment> comments = commentService.findActiveByInquiryId(inquiryId);
|
|
||||||
Map<String, String> writerNames = resolveWriterNames(comments);
|
|
||||||
return comments.stream()
|
|
||||||
.map(c -> toDto(c, current, writerNames))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InquiryCommentDTO createUserComment(String inquiryId, String content, PortalAuthenticatedUser current) {
|
|
||||||
Inquiry inquiry = inquiryService.getAccessibleInquiry(current, inquiryId);
|
|
||||||
permissionChecker.assertWritable(inquiry);
|
|
||||||
InquiryComment saved = commentService.create(inquiry, content, ADMIN_N);
|
|
||||||
publishAdminNotification(inquiry, saved, current);
|
|
||||||
Map<String, String> writerNames = new HashMap<>();
|
|
||||||
writerNames.put(current.getId(), current.getUserName());
|
|
||||||
return toDto(saved, current, writerNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public Map<String, Long> countActiveByInquiryIds(Collection<String> inquiryIds) {
|
|
||||||
if (inquiryIds == null || inquiryIds.isEmpty()) {
|
|
||||||
return Collections.emptyMap();
|
|
||||||
}
|
|
||||||
List<Object[]> rows = commentRepository.countActiveGroupByInquiry(inquiryIds, DEL_N);
|
|
||||||
Map<String, Long> result = new HashMap<>();
|
|
||||||
for (Object[] row : rows) {
|
|
||||||
String inquiryId = (String) row[0];
|
|
||||||
Long count = ((Number) row[1]).longValue();
|
|
||||||
result.put(inquiryId, count);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deleteOwnComment(String commentId, PortalAuthenticatedUser current) {
|
|
||||||
InquiryComment comment = commentService.findById(commentId)
|
|
||||||
.orElseThrow(() -> new NotFoundException("댓글을 찾을 수 없습니다. id=" + commentId));
|
|
||||||
inquiryService.getAccessibleInquiry(current, comment.getInquiry().getId());
|
|
||||||
permissionChecker.assertDeletable(comment, current);
|
|
||||||
commentService.softDelete(commentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void publishAdminNotification(Inquiry inquiry, InquiryComment comment, PortalAuthenticatedUser current) {
|
|
||||||
Map<String, Object> params = new HashMap<>();
|
|
||||||
params.put("inquiryId", inquiry.getId());
|
|
||||||
params.put("inquirySubject", inquiry.getInquirySubject());
|
|
||||||
params.put("commentContent", comment.getCommentDetail());
|
|
||||||
params.put("writerName", current.getUserName());
|
|
||||||
adminNotifier.notifyPortalAdmins(MessageCode.INQUIRY_COMMENT_CREATED, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, String> resolveWriterNames(List<InquiryComment> comments) {
|
|
||||||
Set<String> ids = new HashSet<>();
|
|
||||||
for (InquiryComment c : comments) {
|
|
||||||
String createdBy = c.getCreatedBy();
|
|
||||||
if (createdBy != null && !createdBy.isEmpty()) {
|
|
||||||
ids.add(createdBy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Map<String, String> map = new HashMap<>();
|
|
||||||
for (String id : ids) {
|
|
||||||
String name = lookupWriterName(id);
|
|
||||||
if (name != null && !name.isEmpty()) {
|
|
||||||
map.put(id, name);
|
|
||||||
} else {
|
|
||||||
log.warn("댓글 작성자명 조회 실패 — createdBy={}, isUuid={}", id, isUuid(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String lookupWriterName(String createdBy) {
|
|
||||||
if (isUuid(createdBy)) {
|
|
||||||
String name = portalUserRepository.findById(createdBy)
|
|
||||||
.map(PortalUser::getUserName).orElse(null);
|
|
||||||
if (name != null && !name.isEmpty()) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
return userInfoRepository.findById(createdBy)
|
|
||||||
.map(UserInfo::getUsername).orElse(null);
|
|
||||||
}
|
|
||||||
String name = userInfoRepository.findById(createdBy)
|
|
||||||
.map(UserInfo::getUsername).orElse(null);
|
|
||||||
if (name != null && !name.isEmpty()) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
return portalUserRepository.findById(createdBy)
|
|
||||||
.map(PortalUser::getUserName).orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isUuid(String value) {
|
|
||||||
if (value == null || value.length() != 36) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
UUID.fromString(value);
|
|
||||||
return true;
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private InquiryCommentDTO toDto(InquiryComment entity, PortalAuthenticatedUser current,
|
|
||||||
Map<String, String> writerNames) {
|
|
||||||
String writerId = entity.getCreatedBy();
|
|
||||||
boolean deletable = writerId != null
|
|
||||||
&& writerId.equals(current.getId())
|
|
||||||
&& entity.getInquiry() != null
|
|
||||||
&& !DjbInquiryStatus.isClosed(entity.getInquiry().getInquiryStatus());
|
|
||||||
String writerName = writerId != null ? writerNames.get(writerId) : null;
|
|
||||||
if (writerName == null || writerName.isEmpty()) {
|
|
||||||
writerName = "(알 수 없음)";
|
|
||||||
}
|
|
||||||
return InquiryCommentDTO.builder()
|
|
||||||
.id(entity.getId())
|
|
||||||
.content(entity.getCommentDetail())
|
|
||||||
.writerId(writerId)
|
|
||||||
.writerName(writerName)
|
|
||||||
.adminYn(entity.getAdminYn())
|
|
||||||
.createdDate(entity.getCreatedDate())
|
|
||||||
.deletable(deletable)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-46
@@ -1,46 +0,0 @@
|
|||||||
package com.eactive.apim.portal.djb.community.qna.comment.service;
|
|
||||||
|
|
||||||
import com.eactive.apim.portal.common.exception.NotFoundException;
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.comment.repository.InquiryCommentRepository;
|
|
||||||
import com.eactive.apim.portal.qna.entity.Inquiry;
|
|
||||||
import com.eactive.apim.portal.qna.entity.InquiryComment;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@Transactional
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InquiryCommentService {
|
|
||||||
|
|
||||||
private static final String DEL_N = "N";
|
|
||||||
|
|
||||||
private final InquiryCommentRepository repository;
|
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public List<InquiryComment> findActiveByInquiryId(String inquiryId) {
|
|
||||||
return repository.findByInquiry_IdAndDelYnOrderByCreatedDateAsc(inquiryId, DEL_N);
|
|
||||||
}
|
|
||||||
|
|
||||||
public InquiryComment create(Inquiry inquiry, String content, String adminYn) {
|
|
||||||
InquiryComment comment = new InquiryComment();
|
|
||||||
comment.setInquiry(inquiry);
|
|
||||||
comment.setCommentDetail(content);
|
|
||||||
comment.setAdminYn(adminYn);
|
|
||||||
comment.setDelYn(DEL_N);
|
|
||||||
return repository.save(comment);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<InquiryComment> findById(String commentId) {
|
|
||||||
return repository.findById(commentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void softDelete(String commentId) {
|
|
||||||
InquiryComment comment = repository.findById(commentId)
|
|
||||||
.orElseThrow(() -> new NotFoundException("댓글을 찾을 수 없습니다. id=" + commentId));
|
|
||||||
comment.markDeleted();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.eactive.apim.portal.djb.community.qna.constant;
|
|
||||||
|
|
||||||
public final class DjbAdminRole {
|
|
||||||
|
|
||||||
/** TSEAIRM02.roleidnfiname 컬럼에서 포털 관리자를 식별하는 값. */
|
|
||||||
public static final String PORTAL_ADMIN = "portal-admin";
|
|
||||||
|
|
||||||
private DjbAdminRole() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
package com.eactive.apim.portal.djb.community.qna.constant;
|
|
||||||
|
|
||||||
public final class DjbInquiryStatus {
|
|
||||||
|
|
||||||
public static final String PENDING = "PENDING";
|
|
||||||
public static final String IN_PROGRESS = "IN_PROGRESS";
|
|
||||||
public static final String CLOSED = "CLOSED";
|
|
||||||
|
|
||||||
private DjbInquiryStatus() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isClosed(String status) {
|
|
||||||
return CLOSED.equals(status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
package com.eactive.apim.portal.djb.community.qna.exception;
|
|
||||||
|
|
||||||
public class InquiryClosedException extends RuntimeException {
|
|
||||||
|
|
||||||
public InquiryClosedException() {
|
|
||||||
super("종료된 문의에는 댓글을 작성/삭제할 수 없습니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public InquiryClosedException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
package com.eactive.apim.portal.djb.community.qna.exception;
|
|
||||||
|
|
||||||
public class InquiryCommentNotOwnedException extends RuntimeException {
|
|
||||||
|
|
||||||
public InquiryCommentNotOwnedException() {
|
|
||||||
super("본인이 작성한 댓글만 삭제할 수 있습니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public InquiryCommentNotOwnedException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-45
@@ -1,45 +0,0 @@
|
|||||||
package com.eactive.apim.portal.djb.community.qna.support;
|
|
||||||
|
|
||||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.constant.DjbInquiryStatus;
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.exception.InquiryClosedException;
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.exception.InquiryCommentNotOwnedException;
|
|
||||||
import com.eactive.apim.portal.qna.entity.Inquiry;
|
|
||||||
import com.eactive.apim.portal.qna.entity.InquiryComment;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class InquiryCommentPermissionChecker {
|
|
||||||
|
|
||||||
public boolean canWrite(Inquiry inquiry) {
|
|
||||||
return inquiry != null && !DjbInquiryStatus.isClosed(inquiry.getInquiryStatus());
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean canDelete(InquiryComment comment, PortalAuthenticatedUser current) {
|
|
||||||
if (comment == null || current == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
boolean owner = comment.getCreatedBy() != null
|
|
||||||
&& comment.getCreatedBy().equals(current.getId());
|
|
||||||
boolean notClosed = comment.getInquiry() == null
|
|
||||||
|| !DjbInquiryStatus.isClosed(comment.getInquiry().getInquiryStatus());
|
|
||||||
return owner && notClosed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void assertWritable(Inquiry inquiry) {
|
|
||||||
if (!canWrite(inquiry)) {
|
|
||||||
throw new InquiryClosedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void assertDeletable(InquiryComment comment, PortalAuthenticatedUser current) {
|
|
||||||
if (comment.getInquiry() != null
|
|
||||||
&& DjbInquiryStatus.isClosed(comment.getInquiry().getInquiryStatus())) {
|
|
||||||
throw new InquiryClosedException();
|
|
||||||
}
|
|
||||||
if (comment.getCreatedBy() == null
|
|
||||||
|| !comment.getCreatedBy().equals(current.getId())) {
|
|
||||||
throw new InquiryCommentNotOwnedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6727,232 +6727,6 @@ select.form-control {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inquiry-comment-count {
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 6px;
|
|
||||||
color: #DC2626;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inquiry-header-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #6B7280;
|
|
||||||
}
|
|
||||||
.inquiry-header-right .inquiry-detail-writer {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #374151;
|
|
||||||
}
|
|
||||||
.inquiry-header-right .inquiry-detail-divider {
|
|
||||||
color: #D1D5DB;
|
|
||||||
}
|
|
||||||
.inquiry-header-right .inquiry-detail-date {
|
|
||||||
font-size: inherit;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-section {
|
|
||||||
margin: 40px 0 48px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-list-box {
|
|
||||||
padding: 24px 28px;
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
border: 1px solid #E5E7EB;
|
|
||||||
border-radius: 8px;
|
|
||||||
min-height: 120px;
|
|
||||||
margin-bottom: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-empty {
|
|
||||||
padding: 28px 0;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #9CA3AF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-item {
|
|
||||||
padding: 18px 0;
|
|
||||||
border-bottom: 1px dashed #E5E7EB;
|
|
||||||
}
|
|
||||||
.djb-comment-item:first-child {
|
|
||||||
padding-top: 4px;
|
|
||||||
}
|
|
||||||
.djb-comment-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
}
|
|
||||||
.djb-comment-item--admin .djb-comment-writer {
|
|
||||||
color: #1D4ED8;
|
|
||||||
}
|
|
||||||
.djb-comment-item .djb-comment-meta {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.djb-comment-item .djb-comment-meta-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.djb-comment-item .djb-comment-writer {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #111827;
|
|
||||||
letter-spacing: -0.2px;
|
|
||||||
}
|
|
||||||
.djb-comment-item .djb-comment-admin-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 2px 8px;
|
|
||||||
background-color: #DBEAFE;
|
|
||||||
color: #1D4ED8;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.djb-comment-item .djb-comment-meta-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.djb-comment-item .djb-comment-date {
|
|
||||||
color: #6B7280;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.djb-comment-item .djb-comment-delete-btn {
|
|
||||||
background: none;
|
|
||||||
border: 1px solid #E5E7EB;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 3px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #6B7280;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
.djb-comment-item .djb-comment-delete-btn:hover {
|
|
||||||
background-color: #FEE2E2;
|
|
||||||
border-color: #FCA5A5;
|
|
||||||
color: #B91C1C;
|
|
||||||
}
|
|
||||||
.djb-comment-item .djb-comment-body {
|
|
||||||
padding: 12px 16px;
|
|
||||||
background-color: #F3F4F6;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #1F2937;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-form-wrapper {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-form {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea.djb-comment-input {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 16px 18px;
|
|
||||||
border: 1px solid #D1D5DB;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: #F9FAFB;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #1F2937;
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 120px;
|
|
||||||
font-family: inherit;
|
|
||||||
outline: none;
|
|
||||||
transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
|
|
||||||
}
|
|
||||||
textarea.djb-comment-input::placeholder {
|
|
||||||
color: #9CA3AF;
|
|
||||||
}
|
|
||||||
textarea.djb-comment-input:hover {
|
|
||||||
border-color: #9CA3AF;
|
|
||||||
}
|
|
||||||
textarea.djb-comment-input:focus {
|
|
||||||
border-color: #1D4ED8;
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
box-shadow: 0 0 0 3px rgba(29, 78, 216, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-form-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-char-counter {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #94A3B8;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.djb-comment-submit {
|
|
||||||
display: inline-block;
|
|
||||||
min-width: 140px;
|
|
||||||
padding: 12px 28px;
|
|
||||||
background-color: #1D4ED8;
|
|
||||||
color: #FFFFFF;
|
|
||||||
border: 1px solid #1D4ED8;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.2px;
|
|
||||||
cursor: pointer;
|
|
||||||
appearance: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
line-height: 1.2;
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0 2px 6px rgba(29, 78, 216, 0.18);
|
|
||||||
transition: background-color 0.15s ease, box-shadow 0.15s ease, transform 0.05s ease;
|
|
||||||
}
|
|
||||||
button.djb-comment-submit:hover {
|
|
||||||
background-color: #1E40AF;
|
|
||||||
border-color: #1E40AF;
|
|
||||||
box-shadow: 0 3px 10px rgba(29, 78, 216, 0.28);
|
|
||||||
}
|
|
||||||
button.djb-comment-submit:active {
|
|
||||||
transform: translateY(1px);
|
|
||||||
}
|
|
||||||
button.djb-comment-submit:disabled {
|
|
||||||
background-color: #94A3B8;
|
|
||||||
border-color: #94A3B8;
|
|
||||||
box-shadow: none;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-readonly-msg {
|
|
||||||
margin: 0;
|
|
||||||
padding: 16px 20px;
|
|
||||||
background-color: #F3F4F6;
|
|
||||||
border-radius: 6px;
|
|
||||||
text-align: center;
|
|
||||||
color: #6B7280;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-carousel-section {
|
.hero-carousel-section {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,172 +0,0 @@
|
|||||||
(function () {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const section = document.querySelector('.djb-comment-section');
|
|
||||||
if (!section) return;
|
|
||||||
|
|
||||||
const inquiryId = section.getAttribute('data-inquiry-id');
|
|
||||||
const closed = section.getAttribute('data-closed') === 'true';
|
|
||||||
const listEl = document.getElementById('djbCommentList');
|
|
||||||
const emptyEl = document.getElementById('djbCommentEmpty');
|
|
||||||
const countEl = document.getElementById('djbCommentCount');
|
|
||||||
const formEl = document.getElementById('djbCommentForm');
|
|
||||||
const inputEl = document.getElementById('djbCommentInput');
|
|
||||||
const counterEl = document.getElementById('djbCommentCharCounter');
|
|
||||||
|
|
||||||
function getCsrfToken() {
|
|
||||||
const match = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/);
|
|
||||||
return match ? decodeURIComponent(match[1]) : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchJson(url, options) {
|
|
||||||
options = options || {};
|
|
||||||
const headers = Object.assign({}, options.headers || {});
|
|
||||||
const method = (options.method || 'GET').toUpperCase();
|
|
||||||
if (method !== 'GET' && method !== 'HEAD') {
|
|
||||||
headers['X-XSRF-TOKEN'] = getCsrfToken();
|
|
||||||
}
|
|
||||||
if (options.body && !headers['Content-Type']) {
|
|
||||||
headers['Content-Type'] = 'application/json;charset=UTF-8';
|
|
||||||
}
|
|
||||||
return fetch(url, Object.assign({ credentials: 'same-origin' }, options, { headers: headers }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(text) {
|
|
||||||
if (text == null) return '';
|
|
||||||
return String(text)
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(value) {
|
|
||||||
if (!value) return '';
|
|
||||||
try {
|
|
||||||
const d = new Date(value);
|
|
||||||
if (isNaN(d.getTime())) return '';
|
|
||||||
const pad = function (n) { return n < 10 ? '0' + n : '' + n; };
|
|
||||||
return d.getFullYear() + '.' + pad(d.getMonth() + 1) + '.' + pad(d.getDate())
|
|
||||||
+ ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
|
|
||||||
} catch (e) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function render(comments) {
|
|
||||||
listEl.innerHTML = '';
|
|
||||||
if (!comments || comments.length === 0) {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.className = 'djb-comment-empty';
|
|
||||||
li.textContent = '아직 등록된 댓글이 없습니다.';
|
|
||||||
listEl.appendChild(li);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
comments.forEach(function (c) {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.className = 'djb-comment-item' + (c.adminYn === 'Y' ? ' djb-comment-item--admin' : '');
|
|
||||||
li.setAttribute('data-comment-id', c.id);
|
|
||||||
li.innerHTML = ''
|
|
||||||
+ '<div class="djb-comment-meta">'
|
|
||||||
+ ' <div class="djb-comment-meta-left">'
|
|
||||||
+ ' <span class="djb-comment-writer">' + escapeHtml(c.writerName || '(알 수 없음)') + '</span>'
|
|
||||||
+ (c.adminYn === 'Y' ? ' <span class="djb-comment-admin-badge">관리자</span>' : '')
|
|
||||||
+ ' </div>'
|
|
||||||
+ ' <div class="djb-comment-meta-right">'
|
|
||||||
+ ' <span class="djb-comment-date">' + escapeHtml(formatDate(c.createdDate)) + '</span>'
|
|
||||||
+ (c.deletable ? ' <button type="button" class="djb-comment-delete-btn" data-id="' + escapeHtml(c.id) + '">삭제</button>' : '')
|
|
||||||
+ ' </div>'
|
|
||||||
+ '</div>'
|
|
||||||
+ '<div class="djb-comment-body">' + escapeHtml(c.content) + '</div>';
|
|
||||||
listEl.appendChild(li);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function load() {
|
|
||||||
fetchJson('/djb/inquiry/' + encodeURIComponent(inquiryId) + '/comments')
|
|
||||||
.then(function (res) {
|
|
||||||
if (!res.ok) throw new Error('댓글 조회에 실패했습니다.');
|
|
||||||
return res.json();
|
|
||||||
})
|
|
||||||
.then(render)
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error('[djb-comments] load error', err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSubmit() {
|
|
||||||
const content = (inputEl.value || '').trim();
|
|
||||||
if (!content) {
|
|
||||||
if (window.customPopups) window.customPopups.showAlert('댓글 내용을 입력해 주세요.');
|
|
||||||
inputEl.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fetchJson('/djb/inquiry/' + encodeURIComponent(inquiryId) + '/comments', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ content: content })
|
|
||||||
})
|
|
||||||
.then(function (res) {
|
|
||||||
if (res.status === 409) throw new Error('종료된 문의에는 댓글을 작성할 수 없습니다.');
|
|
||||||
if (!res.ok) throw new Error('댓글 등록에 실패했습니다.');
|
|
||||||
return res.json();
|
|
||||||
})
|
|
||||||
.then(function () {
|
|
||||||
inputEl.value = '';
|
|
||||||
updateCounter();
|
|
||||||
load();
|
|
||||||
})
|
|
||||||
.catch(function (err) {
|
|
||||||
if (window.customPopups) window.customPopups.showAlert(err.message);
|
|
||||||
else alert(err.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDelete(commentId) {
|
|
||||||
if (!commentId) return;
|
|
||||||
function doDelete() {
|
|
||||||
fetchJson('/djb/inquiry/comments/' + encodeURIComponent(commentId), { method: 'DELETE' })
|
|
||||||
.then(function (res) {
|
|
||||||
if (res.status === 403) throw new Error('본인이 작성한 댓글만 삭제할 수 있습니다.');
|
|
||||||
if (res.status === 409) throw new Error('종료된 문의에는 댓글을 삭제할 수 없습니다.');
|
|
||||||
if (!res.ok && res.status !== 204) throw new Error('댓글 삭제에 실패했습니다.');
|
|
||||||
load();
|
|
||||||
})
|
|
||||||
.catch(function (err) {
|
|
||||||
if (window.customPopups) window.customPopups.showAlert(err.message);
|
|
||||||
else alert(err.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (window.customPopups) {
|
|
||||||
window.customPopups.showConfirm('댓글을 삭제하시겠습니까?', function (ok) {
|
|
||||||
if (ok) doDelete();
|
|
||||||
});
|
|
||||||
} else if (window.confirm('댓글을 삭제하시겠습니까?')) {
|
|
||||||
doDelete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCounter() {
|
|
||||||
if (!counterEl || !inputEl) return;
|
|
||||||
const max = inputEl.getAttribute('maxlength') || '2000';
|
|
||||||
counterEl.textContent = (inputEl.value || '').length + ' / ' + max;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!closed && formEl) {
|
|
||||||
formEl.addEventListener('submit', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSubmit();
|
|
||||||
});
|
|
||||||
inputEl.addEventListener('input', updateCounter);
|
|
||||||
updateCounter();
|
|
||||||
}
|
|
||||||
|
|
||||||
listEl.addEventListener('click', function (e) {
|
|
||||||
const target = e.target;
|
|
||||||
if (target && target.classList.contains('djb-comment-delete-btn')) {
|
|
||||||
handleDelete(target.getAttribute('data-id'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
load();
|
|
||||||
})();
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
// -----------------------------------------------------------------------------
|
|
||||||
// DJBank Custom — Q&A 댓글 영역 (DJPGPT0002)
|
|
||||||
// 설계서: DJPCSQ0300P 참조
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// 목록 화면 — 제목 옆 댓글 개수 표시 (설계서 ⑥)
|
|
||||||
.inquiry-comment-count {
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 6px;
|
|
||||||
color: #DC2626;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 상세 헤더 우측 그룹 (작성자 + 날짜)
|
|
||||||
.inquiry-header-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #6B7280;
|
|
||||||
|
|
||||||
.inquiry-detail-writer {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inquiry-detail-divider {
|
|
||||||
color: #D1D5DB;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inquiry-detail-date {
|
|
||||||
font-size: inherit;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-section {
|
|
||||||
margin: 40px 0 48px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 댓글 목록 박스 (설계서 ① 영역) ─────────────────────────
|
|
||||||
.djb-comment-list-box {
|
|
||||||
padding: 24px 28px;
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
border: 1px solid #E5E7EB;
|
|
||||||
border-radius: 8px;
|
|
||||||
min-height: 120px;
|
|
||||||
margin-bottom: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-empty {
|
|
||||||
padding: 28px 0;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #9CA3AF;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 댓글 한 건 (설계서 작성자 + 날짜 + Contents) ───────────
|
|
||||||
.djb-comment-item {
|
|
||||||
padding: 18px 0;
|
|
||||||
border-bottom: 1px dashed #E5E7EB;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
padding-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--admin .djb-comment-writer {
|
|
||||||
color: #1D4ED8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-meta {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-meta-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-writer {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #111827;
|
|
||||||
letter-spacing: -0.2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-admin-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 2px 8px;
|
|
||||||
background-color: #DBEAFE;
|
|
||||||
color: #1D4ED8;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-meta-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-date {
|
|
||||||
color: #6B7280;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-delete-btn {
|
|
||||||
background: none;
|
|
||||||
border: 1px solid #E5E7EB;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 3px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #6B7280;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #FEE2E2;
|
|
||||||
border-color: #FCA5A5;
|
|
||||||
color: #B91C1C;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-body {
|
|
||||||
padding: 12px 16px;
|
|
||||||
background-color: #F3F4F6;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #1F2937;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 작성 영역 (설계서 ②, ③) ───────────────────────────────
|
|
||||||
.djb-comment-form-wrapper {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-form {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea.djb-comment-input {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 16px 18px;
|
|
||||||
border: 1px solid #D1D5DB;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: #F9FAFB;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #1F2937;
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 120px;
|
|
||||||
font-family: inherit;
|
|
||||||
outline: none;
|
|
||||||
transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: #9CA3AF;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: #9CA3AF;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border-color: #1D4ED8;
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
box-shadow: 0 0 0 3px rgba(29, 78, 216, 0.15);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-form-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-char-counter {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #94A3B8;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.djb-comment-submit {
|
|
||||||
display: inline-block;
|
|
||||||
min-width: 140px;
|
|
||||||
padding: 12px 28px;
|
|
||||||
background-color: #1D4ED8;
|
|
||||||
color: #FFFFFF;
|
|
||||||
border: 1px solid #1D4ED8;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.2px;
|
|
||||||
cursor: pointer;
|
|
||||||
appearance: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
line-height: 1.2;
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0 2px 6px rgba(29, 78, 216, 0.18);
|
|
||||||
transition: background-color 0.15s ease, box-shadow 0.15s ease, transform 0.05s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #1E40AF;
|
|
||||||
border-color: #1E40AF;
|
|
||||||
box-shadow: 0 3px 10px rgba(29, 78, 216, 0.28);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: translateY(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
background-color: #94A3B8;
|
|
||||||
border-color: #94A3B8;
|
|
||||||
box-shadow: none;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.djb-comment-readonly-msg {
|
|
||||||
margin: 0;
|
|
||||||
padding: 16px 20px;
|
|
||||||
background-color: #F3F4F6;
|
|
||||||
border-radius: 6px;
|
|
||||||
text-align: center;
|
|
||||||
color: #6B7280;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
@@ -42,7 +42,6 @@
|
|||||||
@use 'components/pagination' as *;
|
@use 'components/pagination' as *;
|
||||||
@use 'components/breadcrumb' as *;
|
@use 'components/breadcrumb' as *;
|
||||||
@use 'components/test-env-notice' as *;
|
@use 'components/test-env-notice' as *;
|
||||||
@use 'components/djb-inquiry-comments' as *;
|
|
||||||
|
|
||||||
// 5. Page-specific styles
|
// 5. Page-specific styles
|
||||||
@use 'pages/index' as *;
|
@use 'pages/index' as *;
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div th:fragment="commentSection(inquiry, commentWritable)" class="djb-comment-section"
|
|
||||||
th:attr="data-inquiry-id=${inquiry.id},data-closed=${inquiry.inquiryStatus == 'CLOSED' ? 'true' : 'false'}">
|
|
||||||
|
|
||||||
<div class="djb-comment-list-box">
|
|
||||||
<ul class="djb-comment-list" id="djbCommentList">
|
|
||||||
<li class="djb-comment-empty" id="djbCommentEmpty">아직 등록된 댓글이 없습니다.</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="djb-comment-form-wrapper" th:if="${commentWritable}">
|
|
||||||
<form id="djbCommentForm" class="djb-comment-form" onsubmit="return false;">
|
|
||||||
<textarea id="djbCommentInput"
|
|
||||||
class="djb-comment-input"
|
|
||||||
rows="4"
|
|
||||||
maxlength="2000"
|
|
||||||
placeholder="내용"
|
|
||||||
required></textarea>
|
|
||||||
<div class="djb-comment-form-actions">
|
|
||||||
<span class="djb-comment-char-counter" id="djbCommentCharCounter">0 / 2000</span>
|
|
||||||
<button type="submit" class="djb-comment-submit">댓글달기</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="djb-comment-form-wrapper djb-comment-readonly" th:unless="${commentWritable}">
|
|
||||||
<p class="djb-comment-readonly-msg">종료된 문의에는 댓글을 작성할 수 없습니다.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -17,20 +17,30 @@
|
|||||||
<div class="inquiry-detail-header">
|
<div class="inquiry-detail-header">
|
||||||
<div class="inquiry-header-top">
|
<div class="inquiry-header-top">
|
||||||
<span class="inquiry-status-badge"
|
<span class="inquiry-status-badge"
|
||||||
th:classappend="${inquiry.inquiryStatus == 'IN_PROGRESS' ? 'inquiry-status-badge--completed' : (inquiry.inquiryStatus == 'CLOSED' ? 'inquiry-status-badge--closed' : 'inquiry-status-badge--pending')}"
|
th:classappend="${inquiry.inquiryStatus == 'RESPONDED' ? 'inquiry-status-badge--completed' : 'inquiry-status-badge--pending'}"
|
||||||
th:text="${inquiry.inquiryStatus == 'IN_PROGRESS' ? '답변완료' : (inquiry.inquiryStatus == 'CLOSED' ? '종료' : '답변대기')}">
|
th:text="${inquiry.inquiryStatus == 'RESPONDED' ? '답변완료' : '답변대기'}">
|
||||||
답변대기
|
답변대기
|
||||||
</span>
|
</span>
|
||||||
<div class="inquiry-header-right">
|
<span class="inquiry-detail-date" th:text="${#temporals.format(inquiry.createdDate, 'yyyy.MM.dd')}">2025.11.01</span>
|
||||||
<span class="inquiry-detail-writer"
|
|
||||||
th:text="${inquiry.inquirerName != null ? inquiry.inquirerName : (inquiry.inquirer != null ? inquiry.inquirer.userName : '')}">작성자</span>
|
|
||||||
<span class="inquiry-detail-divider">|</span>
|
|
||||||
<span class="inquiry-detail-date" th:text="${#temporals.format(inquiry.createdDate, 'yyyy.MM.dd')}">2025.11.01</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<h2 class="inquiry-detail-title" th:text="${inquiry.inquirySubject}">질문 제목</h2>
|
<h2 class="inquiry-detail-title" th:text="${inquiry.inquirySubject}">질문 제목</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Attachment Section -->
|
||||||
|
<div class="inquiry-detail-attachment" th:if="${!#strings.isEmpty(inquiry.attachFile)}">
|
||||||
|
<div class="attachment-list" th:with="fileInfo=${@fileService.findById(inquiry.attachFile)}">
|
||||||
|
<div class="attachment-item" th:each="fileDetail : ${fileInfo.getFileDetails()}">
|
||||||
|
<a th:href="'javascript:fn_downloadFile(\'' + ${fileDetail.fileId} + '\',\''+ ${fileDetail.fileSn} +'\')'" class="inquiry-attachment-link">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M18 15.75C18 17.4833 17.3917 18.9583 16.175 20.175C14.9583 21.3917 13.4833 22 11.75 22C10.0167 22 8.54167 21.3917 7.325 20.175C6.10833 18.9583 5.5 17.4833 5.5 15.75V6.5C5.5 5.25 5.9375 4.1875 6.8125 3.3125C7.6875 2.4375 8.75 2 10 2C11.25 2 12.3125 2.4375 13.1875 3.3125C14.0625 4.1875 14.5 5.25 14.5 6.5V15.25C14.5 16.0167 14.2333 16.6667 13.7 17.2C13.1667 17.7333 12.5167 18 11.75 18C10.9833 18 10.3333 17.7333 9.8 17.2C9.26667 16.6667 9 16.0167 9 15.25V6H11V15.25C11 15.4667 11.0708 15.6458 11.2125 15.7875C11.3542 15.9292 11.5333 16 11.75 16C11.9667 16 12.1458 15.9292 12.2875 15.7875C12.4292 15.6458 12.5 15.4667 12.5 15.25V6.5C12.4833 5.8 12.2375 5.20833 11.7625 4.725C11.2875 4.24167 10.7 4 10 4C9.3 4 8.70833 4.24167 8.225 4.725C7.74167 5.20833 7.5 5.8 7.5 6.5V15.75C7.48333 16.9333 7.89167 17.9375 8.725 18.7625C9.55833 19.5875 10.5667 20 11.75 20C12.9167 20 13.9083 19.5875 14.725 18.7625C15.5417 17.9375 15.9667 16.9333 16 15.75V6H18V15.75Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
<span class="attachment-label">첨부파일</span>
|
||||||
|
<span class="attachment-filename">[[${fileDetail.originalFileName}]].[[${fileDetail.fileExtension}]]</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Inquiry Content Section -->
|
<!-- Inquiry Content Section -->
|
||||||
<div class="inquiry-detail-content">
|
<div class="inquiry-detail-content">
|
||||||
<div id="inquiryDetail" class="inquiry-content-body" th:utext="${inquiry.inquiryDetail}">
|
<div id="inquiryDetail" class="inquiry-content-body" th:utext="${inquiry.inquiryDetail}">
|
||||||
@@ -39,7 +49,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Admin Response Section -->
|
<!-- Admin Response Section -->
|
||||||
<div class="inquiry-response-section" th:if="${inquiry.inquiryStatus == 'IN_PROGRESS' or inquiry.inquiryStatus == 'CLOSED'}">
|
<div class="inquiry-response-section" th:if="${inquiry.inquiryStatus == 'RESPONDED'}">
|
||||||
<div class="inquiry-response-header">
|
<div class="inquiry-response-header">
|
||||||
<span class="response-label">관리자 답변</span>
|
<span class="response-label">관리자 답변</span>
|
||||||
<span class="response-date" th:text="${#temporals.format(inquiry.responseDate, 'yyyy.MM.dd')}">2025.11.02</span>
|
<span class="response-date" th:text="${#temporals.format(inquiry.responseDate, 'yyyy.MM.dd')}">2025.11.02</span>
|
||||||
@@ -49,15 +59,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Comment Section (DJBank Custom DJPGPT0002) -->
|
|
||||||
<th:block th:replace="~{apps/community/djb/fragments-inquiry-comments :: commentSection(${inquiry}, ${commentWritable})}"></th:block>
|
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="inquiry-detail-actions">
|
<div class="inquiry-detail-actions">
|
||||||
<div class="actions-left">
|
<div class="actions-left">
|
||||||
<button type="button" class="btn-inquiry-list" th:onclick="|location.href='@{/inquiry}'|">목록</button>
|
<button type="button" class="btn-inquiry-list" th:onclick="|location.href='@{/inquiry}'|">목록</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions-right" th:if="${inquiry.inquiryStatus == 'PENDING' && inquiry.inquirer.id == #authentication.principal.id}">
|
<div class="actions-right" th:if="${inquiry.inquiryStatus != 'RESPONDED' && inquiry.inquirer.id == #authentication.principal.id}">
|
||||||
<form id="deleteForm" th:action="@{/inquiry/{id}/delete(id=${inquiry.id})}" method="post" style="display: none;"></form>
|
<form id="deleteForm" th:action="@{/inquiry/{id}/delete(id=${inquiry.id})}" method="post" style="display: none;"></form>
|
||||||
<button type="button" class="btn-inquiry-edit" th:onclick="|location.href='@{/inquiry/edit(id=${inquiry.id})}'|">수정</button>
|
<button type="button" class="btn-inquiry-edit" th:onclick="|location.href='@{/inquiry/edit(id=${inquiry.id})}'|">수정</button>
|
||||||
<button type="button" class="btn-inquiry-delete" onclick="confirmDelete()">삭제</button>
|
<button type="button" class="btn-inquiry-delete" onclick="confirmDelete()">삭제</button>
|
||||||
@@ -80,6 +87,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
function fn_downloadFile(fileId, fileSn) {
|
||||||
|
window.open('[[@{/file/download}]]' + "?fileId=" + fileId + "&fileSn=" + fileSn);
|
||||||
|
}
|
||||||
|
|
||||||
function decodeHTMLEntities(text) {
|
function decodeHTMLEntities(text) {
|
||||||
var textArea = document.createElement('textarea');
|
var textArea = document.createElement('textarea');
|
||||||
textArea.innerHTML = text;
|
textArea.innerHTML = text;
|
||||||
@@ -106,7 +117,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<script th:src="@{/js/djb/inquiry-comments.js}"></script>
|
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<!-- Form Content -->
|
<!-- Form Content -->
|
||||||
<div class="register-form-container">
|
<div class="register-form-container">
|
||||||
<form name="inquiryForm" id="inquiryForm" th:action="${isNew}? @{/inquiry} : @{/inquiry/edit}"
|
<form name="inquiryForm" id="inquiryForm" th:action="${isNew}? @{/inquiry} : @{/inquiry/edit}"
|
||||||
th:object="${inquiry}" method="post" class="register-form">
|
th:object="${inquiry}" enctype="multipart/form-data" method="post" class="register-form">
|
||||||
<input type="hidden" th:field="*{id}" th:if="${!isNew}">
|
<input type="hidden" th:field="*{id}" th:if="${!isNew}">
|
||||||
|
|
||||||
<!-- Subject Field -->
|
<!-- Subject Field -->
|
||||||
@@ -62,6 +62,53 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- File Upload Field -->
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-label-wrapper label-offset">
|
||||||
|
<span class="form-label-text">첨부파일</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-field-wrapper">
|
||||||
|
<div class="file-upload-inline">
|
||||||
|
<div class="file-input-display" id="fileInputDisplay">
|
||||||
|
<span class="file-display-text" id="fileDisplayText">선택된 파일이 없습니다</span>
|
||||||
|
<button type="button" class="btn-remove-file-inline" id="btnRemoveFile" title="파일 삭제" style="display: none;">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-file-attach" onclick="document.getElementById('file').click()">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<rect width="20" height="20" fill="url(#pattern0_1161_9889)"/>
|
||||||
|
<defs>
|
||||||
|
<pattern id="pattern0_1161_9889" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||||
|
<use xlink:href="#image0_1161_9889" transform="scale(0.0217391)"/>
|
||||||
|
</pattern>
|
||||||
|
<image id="image0_1161_9889" width="46" height="46" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC4AAAAuCAYAAABXuSs3AAAACXBIWXMAABcSAAAXEgFnn9JSAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAeRJREFUeNrsme1tgzAQhiHqAGxQRiATlGxAJ6i7QTpByQTtBrBBRiCdADYInaB0AvcsnaUTNR82GPzDr2Q5ODh6cO7LJgwWiHMeQZfgZReGYRO4LAEMreD/dYeWuQodQ/vh4ypcBK8JYC4eBMczXHGpzCXojICdB0xIwtcugX9IWx65h8kns8nyMAOWQRfjZUbG84Epj+QeYevfA/eJKPRpK3LU3K4KG+AV30ZG8OEAtEgq1Llu0L5m/qYwFYafS4WpvBDTkyrBbF7XWG1GVqTWnJuSuanGP6m18oeBcboi3UZBi+nAH3aOsCKqNCbwe4P/QjuZwB8MQiTDqnANPUE7o/Mbmw0FzInTVGT8OuVIC5xTJbaWqUQK59WVjrPHxinfgi69RegrGfluP3DcJT2PZWzoUtejirE8uAe3BN5ORIR+CGtcAZelbaJKDphR3yU0RJDOCXAAKckqFphhI5kxoavIil/22BQrU77cZNg8V+mVBPlqzokJ5KgojGQ6f1tlN2MjcwKYcNITHgbJ1sL4bSurWJTy8QFaH8c9uAf34B7cg3twD74xOK0/4hWP3OYonlUnDdTEYvK9V642G0FT8KP222ryhm0vVUt2QnvBX6fMM5wBHwfLDjl11WKdP6o/AQYA6KS7NOFJR28AAAAASUVORK5CYII="/>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
파일첨부
|
||||||
|
</button>
|
||||||
|
<input type="hidden" th:field="*{attachFile}">
|
||||||
|
<input type="file"
|
||||||
|
id="file"
|
||||||
|
name="files"
|
||||||
|
th:accept="${isInternalUser ? '*' : '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.hwp,.gif,.jpg,.jpeg,.png'}"
|
||||||
|
style="display: none;">
|
||||||
|
</div>
|
||||||
|
<div class="form-help-text">
|
||||||
|
<p><i class="fas fa-check-circle"></i>
|
||||||
|
<span th:text="'첨부파일은 ' + ${@portalProperties.file.maxSize} + ' 이내로 등록해 주세요.'">첨부파일은 8MB 이내로 등록해 주세요.</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
문서파일과 이미지 파일만 등록 가능합니다. (pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -87,6 +134,11 @@
|
|||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
$(function () {
|
$(function () {
|
||||||
const btnSubmit = document.querySelector('.btn-submit-form');
|
const btnSubmit = document.querySelector('.btn-submit-form');
|
||||||
|
const fileInput = document.getElementById('file');
|
||||||
|
const fileDisplayText = document.getElementById('fileDisplayText');
|
||||||
|
const fileInputDisplay = document.getElementById('fileInputDisplay');
|
||||||
|
const btnRemoveFile = document.getElementById('btnRemoveFile');
|
||||||
|
const attachFileInput = document.querySelector('input[name="attachFile"]');
|
||||||
const inquiryDetailTextarea = document.getElementById('inquiryDetail');
|
const inquiryDetailTextarea = document.getElementById('inquiryDetail');
|
||||||
const charCounter = document.querySelector('.char-counter');
|
const charCounter = document.querySelector('.char-counter');
|
||||||
|
|
||||||
@@ -96,11 +148,13 @@
|
|||||||
const maxLength = inquiryDetailTextarea.getAttribute('maxlength');
|
const maxLength = inquiryDetailTextarea.getAttribute('maxlength');
|
||||||
charCounter.textContent = currentLength + ' / ' + maxLength;
|
charCounter.textContent = currentLength + ' / ' + maxLength;
|
||||||
|
|
||||||
|
// Character counter for textarea
|
||||||
inquiryDetailTextarea.addEventListener('input', function() {
|
inquiryDetailTextarea.addEventListener('input', function() {
|
||||||
const currentLength = this.value.length;
|
const currentLength = this.value.length;
|
||||||
const maxLength = this.getAttribute('maxlength');
|
const maxLength = this.getAttribute('maxlength');
|
||||||
charCounter.textContent = currentLength + ' / ' + maxLength;
|
charCounter.textContent = currentLength + ' / ' + maxLength;
|
||||||
|
|
||||||
|
// Change color when near limit
|
||||||
if (currentLength > maxLength * 0.9) {
|
if (currentLength > maxLength * 0.9) {
|
||||||
charCounter.style.color = '#FF6B6B';
|
charCounter.style.color = '#FF6B6B';
|
||||||
} else {
|
} else {
|
||||||
@@ -109,6 +163,15 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show existing file if present
|
||||||
|
const existingFile = /*[[${!#strings.isEmpty(inquiry.attachFile) ? @fileService.findById(inquiry.attachFile).fileDetails[0].originalFileName + '.' + @fileService.findById(inquiry.attachFile).fileDetails[0].fileExtension : ''}]]*/ '';
|
||||||
|
if (existingFile) {
|
||||||
|
fileDisplayText.textContent = existingFile;
|
||||||
|
fileDisplayText.classList.add('has-file');
|
||||||
|
fileInputDisplay.classList.add('has-file');
|
||||||
|
btnRemoveFile.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
// Form submission handler
|
// Form submission handler
|
||||||
btnSubmit.addEventListener('click', function (event) {
|
btnSubmit.addEventListener('click', function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -117,6 +180,7 @@
|
|||||||
const inquirySubject = document.getElementById('inquirySubject').value.trim();
|
const inquirySubject = document.getElementById('inquirySubject').value.trim();
|
||||||
const inquiryDetail = document.getElementById('inquiryDetail').value;
|
const inquiryDetail = document.getElementById('inquiryDetail').value;
|
||||||
|
|
||||||
|
// Validation
|
||||||
if (!inquirySubject) {
|
if (!inquirySubject) {
|
||||||
customPopups.showAlert('제목을 작성해 주세요.');
|
customPopups.showAlert('제목을 작성해 주세요.');
|
||||||
document.getElementById('inquirySubject').focus();
|
document.getElementById('inquirySubject').focus();
|
||||||
@@ -131,6 +195,47 @@
|
|||||||
form.submit();
|
form.submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', function () {
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
/*[# th:if="${!isInternalUser}"]*/
|
||||||
|
const allowedExtensions = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.hwp', '.gif', '.jpg', '.jpeg', '.png'];
|
||||||
|
const fileExt = '.' + file.name.split('.').pop().toLowerCase();
|
||||||
|
|
||||||
|
if (!allowedExtensions.includes(fileExt)) {
|
||||||
|
customPopups.showAlert("허용된 파일 형식이 아닙니다.\n문서파일(pdf, doc, docx, xls, xlsx, ppt, pptx, hwp)과 이미지 파일(gif, jpg, png)만 등록 가능합니다.");
|
||||||
|
fileInput.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/*[/]*/
|
||||||
|
|
||||||
|
fileDisplayText.textContent = file.name;
|
||||||
|
fileDisplayText.classList.add('has-file');
|
||||||
|
fileInputDisplay.classList.add('has-file');
|
||||||
|
btnRemoveFile.style.display = 'flex';
|
||||||
|
|
||||||
|
if (attachFileInput) {
|
||||||
|
attachFileInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// File remove handler
|
||||||
|
btnRemoveFile.addEventListener('click', function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
fileInput.value = '';
|
||||||
|
fileDisplayText.textContent = '선택된 파일이 없습니다';
|
||||||
|
fileDisplayText.classList.remove('has-file');
|
||||||
|
fileInputDisplay.classList.remove('has-file');
|
||||||
|
btnRemoveFile.style.display = 'none';
|
||||||
|
|
||||||
|
// Clear attachFile hidden input
|
||||||
|
if (attachFileInput) {
|
||||||
|
attachFileInput.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Focus on subject field
|
// Focus on subject field
|
||||||
document.getElementById('inquirySubject').focus();
|
document.getElementById('inquirySubject').focus();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,16 +62,18 @@
|
|||||||
<a th:href="@{/inquiry/detail(id=${inquiry.id})}" class="notice-title-link">
|
<a th:href="@{/inquiry/detail(id=${inquiry.id})}" class="notice-title-link">
|
||||||
<span class="notice-number" th:text="|[${page.totalElements - (page.number * page.size) - status.index}]|">[1]</span>
|
<span class="notice-number" th:text="|[${page.totalElements - (page.number * page.size) - status.index}]|">[1]</span>
|
||||||
<span th:text="${inquiry.inquirySubject}">질문 제목</span>
|
<span th:text="${inquiry.inquirySubject}">질문 제목</span>
|
||||||
<span class="inquiry-comment-count"
|
<span class="file-icon" th:if="${!#strings.isEmpty(inquiry.attachFile)}">
|
||||||
th:if="${commentCounts != null and commentCounts.get(inquiry.id) != null and commentCounts.get(inquiry.id) > 0}"
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
th:text="|[${commentCounts.get(inquiry.id)}]|">[3]</span>
|
<path d="M18 15.75C18 17.4833 17.3917 18.9583 16.175 20.175C14.9583 21.3917 13.4833 22 11.75 22C10.0167 22 8.54167 21.3917 7.325 20.175C6.10833 18.9583 5.5 17.4833 5.5 15.75V6.5C5.5 5.25 5.9375 4.1875 6.8125 3.3125C7.6875 2.4375 8.75 2 10 2C11.25 2 12.3125 2.4375 13.1875 3.3125C14.0625 4.1875 14.5 5.25 14.5 6.5V15.25C14.5 16.0167 14.2333 16.6667 13.7 17.2C13.1667 17.7333 12.5167 18 11.75 18C10.9833 18 10.3333 17.7333 9.8 17.2C9.26667 16.6667 9 16.0167 9 15.25V6H11V15.25C11 15.4667 11.0708 15.6458 11.2125 15.7875C11.3542 15.9292 11.5333 16 11.75 16C11.9667 16 12.1458 15.9292 12.2875 15.7875C12.4292 15.6458 12.5 15.4667 12.5 15.25V6.5C12.4833 5.8 12.2375 5.20833 11.7625 4.725C11.2875 4.24167 10.7 4 10 4C9.3 4 8.70833 4.24167 8.225 4.725C7.74167 5.20833 7.5 5.8 7.5 6.5V15.75C7.48333 16.9333 7.89167 17.9375 8.725 18.7625C9.55833 19.5875 10.5667 20 11.75 20C12.9167 20 13.9083 19.5875 14.725 18.7625C15.5417 17.9375 15.9667 16.9333 16 15.75V6H18V15.75Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-cell" style="width: 100px;" data-label="작성자" th:text="${inquiry.maskedInquirerName}">홍**</div>
|
<div class="row-cell" style="width: 100px;" data-label="작성자" th:text="${inquiry.maskedInquirerName}">홍**</div>
|
||||||
<div class="row-cell" style="width: 120px;" data-label="처리상태">
|
<div class="row-cell" style="width: 120px;" data-label="처리상태">
|
||||||
<span class="inquiry-status-badge"
|
<span class="inquiry-status-badge"
|
||||||
th:classappend="${inquiry.inquiryStatus == 'IN_PROGRESS' ? 'inquiry-status-badge--completed' : (inquiry.inquiryStatus == 'CLOSED' ? 'inquiry-status-badge--closed' : 'inquiry-status-badge--pending')}"
|
th:classappend="${inquiry.inquiryStatus == 'RESPONDED' ? 'inquiry-status-badge--completed' : 'inquiry-status-badge--pending'}"
|
||||||
th:text="${inquiry.inquiryStatus == 'IN_PROGRESS' ? '답변완료' : (inquiry.inquiryStatus == 'CLOSED' ? '종료' : '답변대기')}">
|
th:text="${inquiry.inquiryStatus == 'RESPONDED' ? '답변완료' : '답변대기'}">
|
||||||
답변대기
|
답변대기
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
-117
@@ -1,117 +0,0 @@
|
|||||||
package com.eactive.apim.portal.djb.community.qna.support;
|
|
||||||
|
|
||||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.exception.InquiryClosedException;
|
|
||||||
import com.eactive.apim.portal.djb.community.qna.exception.InquiryCommentNotOwnedException;
|
|
||||||
import com.eactive.apim.portal.qna.entity.Inquiry;
|
|
||||||
import com.eactive.apim.portal.qna.entity.InquiryComment;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
class InquiryCommentPermissionCheckerTest {
|
|
||||||
|
|
||||||
private InquiryCommentPermissionChecker checker;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
checker = new InquiryCommentPermissionChecker();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Inquiry inquiry(String status) {
|
|
||||||
Inquiry i = new Inquiry();
|
|
||||||
i.setId("inq-1");
|
|
||||||
i.setInquiryStatus(status);
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
private InquiryComment comment(String createdBy, Inquiry inquiry) {
|
|
||||||
InquiryComment c = new InquiryComment();
|
|
||||||
c.setId("cmt-1");
|
|
||||||
c.setInquiry(inquiry);
|
|
||||||
c.setCommentDetail("test");
|
|
||||||
c.setAdminYn("N");
|
|
||||||
c.setDelYn("N");
|
|
||||||
c.setCreatedBy(createdBy);
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
private PortalAuthenticatedUser user(String id) {
|
|
||||||
PortalAuthenticatedUser u = new PortalAuthenticatedUser();
|
|
||||||
u.setId(id);
|
|
||||||
u.setLoginId(id);
|
|
||||||
return u;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void canWrite_pendingTrue() {
|
|
||||||
assertTrue(checker.canWrite(inquiry("PENDING")));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void canWrite_inProgressTrue() {
|
|
||||||
assertTrue(checker.canWrite(inquiry("IN_PROGRESS")));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void canWrite_closedFalse() {
|
|
||||||
assertFalse(checker.canWrite(inquiry("CLOSED")));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void canDelete_ownerInOpenTrue() {
|
|
||||||
Inquiry i = inquiry("IN_PROGRESS");
|
|
||||||
InquiryComment c = comment("alice", i);
|
|
||||||
assertTrue(checker.canDelete(c, user("alice")));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void canDelete_otherUserFalse() {
|
|
||||||
Inquiry i = inquiry("IN_PROGRESS");
|
|
||||||
InquiryComment c = comment("alice", i);
|
|
||||||
assertFalse(checker.canDelete(c, user("bob")));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void canDelete_closedFalse() {
|
|
||||||
Inquiry i = inquiry("CLOSED");
|
|
||||||
InquiryComment c = comment("alice", i);
|
|
||||||
assertFalse(checker.canDelete(c, user("alice")));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void assertWritable_closedThrows() {
|
|
||||||
assertThrows(InquiryClosedException.class, () -> checker.assertWritable(inquiry("CLOSED")));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void assertWritable_pendingOk() {
|
|
||||||
assertDoesNotThrow(() -> checker.assertWritable(inquiry("PENDING")));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void assertDeletable_closedThrowsClosed() {
|
|
||||||
Inquiry i = inquiry("CLOSED");
|
|
||||||
InquiryComment c = comment("alice", i);
|
|
||||||
assertThrows(InquiryClosedException.class, () -> checker.assertDeletable(c, user("alice")));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void assertDeletable_notOwnerThrowsNotOwned() {
|
|
||||||
Inquiry i = inquiry("IN_PROGRESS");
|
|
||||||
InquiryComment c = comment("alice", i);
|
|
||||||
assertThrows(InquiryCommentNotOwnedException.class,
|
|
||||||
() -> checker.assertDeletable(c, user("bob")));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void assertDeletable_ownerInOpenOk() {
|
|
||||||
Inquiry i = inquiry("IN_PROGRESS");
|
|
||||||
InquiryComment c = comment("alice", i);
|
|
||||||
assertDoesNotThrow(() -> checker.assertDeletable(c, user("alice")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user