File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
package kr.itn.itnhub.feed;
import kr.itn.itnhub.mattermost.MattermostClient;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* 기관관리 상세의 게시물/자료 탭이 부르는 읽기 전용 엔드포인트.
* 브라우저는 Mattermost를 절대 직접 호출하지 않고 전부 이 컨트롤러를 거친다.
*/
@RestController
public class ChannelFeedController {
private final ChannelFeedService feedService;
private final MattermostClient mattermost;
public ChannelFeedController(ChannelFeedService feedService, MattermostClient mattermost) {
this.feedService = feedService;
this.mattermost = mattermost;
}
@GetMapping("/api/orgs/{id}/posts")
public List<PostView> posts(@PathVariable Long id, @RequestParam String channel) {
return feedService.posts(id, channel);
}
@GetMapping("/api/orgs/{id}/files")
public List<FileView> files(@PathVariable Long id, @RequestParam String channel) {
return feedService.files(id, channel);
}
/**
* 메시지를 보낸다. {@code message}와 {@code files} 둘 다 비어 있으면 400을 돌려준다.
* {@code notice}가 참이면 정책 A(단일 활성 공지)를 적용한다 - 자세한 내용은
* {@link ChannelFeedService#sendMessage}를 참고.
*/
@PostMapping("/api/orgs/{id}/messages")
public SendResult sendMessage(
@PathVariable Long id,
@RequestParam String channel,
@RequestParam(required = false, defaultValue = "") String message,
@RequestParam(required = false, defaultValue = "false") boolean notice,
@RequestParam(required = false) List<MultipartFile> files) {
List<MultipartFile> submitted = files == null ? List.of() : files;
boolean anyFile = submitted.stream().anyMatch(f -> !f.isEmpty());
if (message.isBlank() && !anyFile) {
throw new InvalidMessageException("메시지 내용이나 첨부파일 중 하나는 있어야 합니다.");
}
List<UploadedFile> uploaded = new ArrayList<>();
for (MultipartFile file : submitted) {
if (file.isEmpty()) {
continue;
}
try {
uploaded.add(new UploadedFile(file.getOriginalFilename(), file.getBytes(), file.getContentType()));
} catch (IOException e) {
throw new InvalidMessageException("첨부파일을 읽지 못했습니다: " + file.getOriginalFilename());
}
}
return feedService.sendMessage(id, channel, message, uploaded, notice);
}
/** 채널의 현재 공지(가장 최근에 고정된 게시글). 없으면 204를 돌려준다. */
@GetMapping("/api/orgs/{id}/notice")
public ResponseEntity<PostView> notice(@PathVariable Long id, @RequestParam String channel) {
PostView notice = feedService.currentNotice(id, channel);
return notice == null ? ResponseEntity.noContent().build() : ResponseEntity.ok(notice);
}
/** 공지 해제: 고정을 전부 풀고 헤더를 비운다. 원본 글은 채팅 기록에 남는다. */
@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping("/api/orgs/{id}/notice")
public void clearNotice(@PathVariable Long id, @RequestParam String channel) {
feedService.clearNotice(id, channel);
}
/**
* 한글 파일명이 대부분이라 {@code Content-Disposition}은 반드시 RFC 5987의
* {@code filename*=UTF-8''...} 형식으로 인코딩해야 한다. 일반 {@code filename="..."}
* 만 쓰면 브라우저마다 한글이 깨진다.
*/
@GetMapping("/api/files/{fileId}")
public ResponseEntity<byte[]> download(@PathVariable String fileId) {
FileRef info = mattermost.fileInfo(fileId);
byte[] content = mattermost.downloadFile(fileId);
String encodedName = URLEncoder.encode(info.name(), StandardCharsets.UTF_8)
.replace("+", "%20");
MediaType contentType = resolveContentType(info.mimeType());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedName)
.contentType(contentType)
.body(content);
}
private MediaType resolveContentType(String mimeType) {
if (mimeType == null || mimeType.isBlank()) {
return MediaType.APPLICATION_OCTET_STREAM;
}
try {
return MediaType.parseMediaType(mimeType);
} catch (org.springframework.util.InvalidMimeTypeException e) {
return MediaType.APPLICATION_OCTET_STREAM;
}
}
}