v9 패턴의 응용
myboard 는 두 컬럼이 비어 있음두 가지를 「기존 테이블에 ALTER 로 추가」 하면서 v9 의 fetch 패턴을 그대로 응용합니다.
ALTER TABLE myboard
ADD COLUMN view_count INT DEFAULT 0;
👉 DEFAULT 0 덕분에 기존 글들도 자동으로 0 부터 시작. 새로 만들 필요 없음.
// com.smhrd.domain.Board — 필드 한 줄 추가
private int viewCount;
👉 카멜 케이스 매핑: view_count ↔ viewCount (MyBatis mapUnderscoreToCamelCase).
@PostMapping("/api/boards/{num}/view")
public Map<String, Integer> incrementView(@PathVariable int num) {
int newCount = service.incrementViewCount(num);
return Map.of("viewCount", newCount);
}
// 글 상세 페이지 진입 시
const BOARD_NUM = ${board.num};
fetch(`/api/boards/${BOARD_NUM}/view`, {method: 'POST'})
.then(r => r.json())
.then(d => {
document.querySelector('#viewCount').textContent = d.viewCount;
});
👉 페이지 들어가자마자 +1, 화면도 즉시 갱신.
@Service
public class BoardService {
@Autowired private BoardMapper mapper;
@Transactional
public int incrementViewCount(int num) {
mapper.incrementViewCount(num);
return mapper.selectOne(num).getViewCount();
}
}
<update id="incrementViewCount" parameterType="int">
UPDATE myboard SET view_count = view_count + 1 WHERE num = #{num}
</update>
ALTER TABLE myboard
ADD COLUMN photo VARCHAR(200) NULL;
👉 NULL 허용이 핵심. 기존 글 전부 사진이 없으니 NULL 로 두고, 새로 올린 글만 파일명을 채움.
// com.smhrd.domain.Board — 한 줄 더 추가
private String photo; // NULL 가능 — 사진 없는 글
<update id="update" parameterType="com.smhrd.domain.Board">
UPDATE myboard
SET title = #{title}, content = #{content}
<if test="photo != null">
, photo = #{photo}
</if>
WHERE num = #{num}
</update>
👉 글만 수정하면 사진은 그대로, 새 사진을 첨부했을 때만 photo 컬럼도 함께 변경. MyBatis 동적 SQL 의 첫 등장.
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
<!-- root-context.xml -->
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="10485760" /> <!-- 10 MB -->
<property name="defaultEncoding" value="UTF-8" />
</bean>
👉 maxUploadSize 로 파일 크기 제한.
@PostMapping("/api/boards/upload")
public Map<String, String> upload(@RequestParam("file") MultipartFile file)
throws IOException {
if (file.isEmpty()) {
throw new IllegalArgumentException("파일 없음");
}
String original = file.getOriginalFilename();
String saved = UUID.randomUUID() + "_" + original;
File dest = new File("/uploads/" + saved);
file.transferTo(dest);
// 응답 JSON 의 filename 을 Board.photo 에 넣어 저장하면 됨
return Map.of(
"filename", saved,
"originalName", original,
"size", String.valueOf(file.getSize())
);
}
<input type="file" id="fileInput" />
<button onclick="upload()">업로드</button>
<div id="result"></div>
async function upload() {
const input = document.getElementById('fileInput');
if (!input.files[0]) return;
const formData = new FormData();
formData.append('file', input.files[0]);
const res = await fetch('/api/boards/upload', {
method: 'POST',
body: formData // ⭐ Content-Type 자동 설정
});
const data = await res.json();
document.getElementById('result').innerHTML =
`업로드 완료: ${data.originalName}`;
}
FormData 객체에 파일을 추가하면:
multipart/form-data; boundary=...
formData.append('file', input.files[0]);
formData.append('description', '설명 텍스트');
formData.append('boardnum', 3);
function uploadWithProgress(file) {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('file', file);
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
document.getElementById('progress').textContent =
`${percent.toFixed(1)}%`;
}
};
/* 다음 슬라이드 — 완료 처리 */
}
👉 upload.onprogress 가 보낸 바이트 / 전체 바이트로 진행률을 알려줍니다.
xhr.onload = () => {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
// 완료 처리
}
};
xhr.open('POST', '/api/boards/upload');
xhr.send(formData);
}
👉 fetch 는 진행률 안 됨. 큰 파일은 XMLHttpRequest 또는 라이브러리 (axios 등).
v10 은 새 패턴 학습이 아니라 — 기존 테이블에 컬럼을 더하는 ALTER + v9 의 fetch 패턴 반복.
<if test="photo != null"> 와 짝| 기능 | v6 (JSP) | v10 (REST + 비동기) |
|---|---|---|
| 목록·상세 | ✓ | ✓ |
| 작성·수정·삭제 | ✓ | ✓ |
| 본인 확인 | ✓ | ✓ |
| 페이징 | v7 | v7 |
| 댓글 (비동기) | — | v9 |
| 조회수 | — | v10 ⭐ (ALTER + view_count) |
| 사진 첨부 | — | v10 ⭐ (ALTER + photo) |
👉 우리 게시판이 실용적인 수준에 도달.
myboard 4 컬럼 + created_at. 조회수·사진 없음.
myboard 에 view_count·photo 두 컬럼이 ALTER 로 추가. 동적 SQL 첫 등장.
<if test="photo != null"> — NULL 허용 컬럼의 짝