이 레슨의 세 파일에 직접 코드를 덧붙이는 문제 5개입니다. 정답을 보기 전에 먼저 힌트만 보고 스스로 작성해 보세요.
StaticHandler.send 에 X-Content-Type-Options: nosniff 헤더와, 파일 내용의 해시로 만든 ETag 헤더를 추가하세요. 힌트: 해시는 MessageDigest.getInstance("SHA-256") 로 바이트 배열을 넣고 앞 8바이트만 16진수로 잘라 씁니다.
static void send(HttpExchange ex, int status, String type, byte[] body) throws IOException {
ex.getResponseHeaders().set("Content-Type", type);
ex.getResponseHeaders().set("X-Content-Type-Options", "nosniff");
if (status == 200) {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(body);
StringBuilder etag = new StringBuilder("\"");
for (int i = 0; i < 8; i++) etag.append(String.format("%02x", hash[i]));
ex.getResponseHeaders().set("ETag", etag.append("\"").toString());
}
ex.sendResponseHeaders(status, body.length);
try (OutputStream os = ex.getResponseBody()) { os.write(body); }
}nosniff 는 브라우저가 Content-Type 을 무시하고 내용을 추측 실행하는 것을 막습니다. ETag 는 파일이 안 바뀌었으면 재전송을 줄이는 조건부 요청(If-None-Match)의 기반입니다.
app.js 에 computed 로 완료된 항목만 걸러내는 doneTodos 를 추가하고, 화면에 필터 토글 버튼을 넣어 전체·완료만 전환하세요. 힌트: 원본 배열은 그대로 두고, 화면에는 필터를 적용한 파생 값만 보여줍니다.
const showDoneOnly = ref(false);
const visibleTodos = computed(() =>
showDoneOnly.value ? todos.value.filter(t => t.done) : todos.value);
// 반환 목록에 showDoneOnly, visibleTodos 추가
// 템플릿에서는 v-for="t in visibleTodos" 로 교체원본 todos 는 그대로 두고 computed 로 파생 목록만 만들면, 필터를 꺼도 원본 데이터가 보존됩니다.
목록 항목의 제목을 더블클릭하면 입력창으로 바뀌고, Enter 를 누르면 PATCH /api/todos/{id} 로 제목을 수정하는 기능을 추가하세요. 힌트: 편집 중인 항목의 id 를 별도 ref 로 기억해 조건 분기에 씁니다.
<span v-if="editingId !== t.id" @dblclick="editingId = t.id">{{ t.title }}</span>
<input v-else v-model="editTitle" @keyup.enter="saveTitle(t)" @blur="editingId = null">const editingId = ref(null);
const editTitle = ref('');
const saveTitle = (t) => run(async () => {
await api('PATCH', `/api/todos/${t.id}`, { title: editTitle.value });
editingId.value = null;
});TodoApi.PATCH 는 이미 title 을 부분 수정으로 받으므로 서버 코드는 그대로 재사용됩니다.
요청 헤더 Accept-Encoding 에 gzip 이 있으면 응답을 GZIPOutputStream 으로 압축해 보내세요. Vue 라이브러리처럼 큰 파일에서 전송량이 크게 줄어듭니다. 힌트: 압축 여부와 상관없이 Content-Length 는 실제로 보낼 바이트 수와 같아야 합니다.
String accept = ex.getRequestHeaders().getFirst("Accept-Encoding");
if (accept != null && accept.contains("gzip")) {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
try (GZIPOutputStream gz = new GZIPOutputStream(buf)) { gz.write(body); }
ex.getResponseHeaders().set("Content-Encoding", "gzip");
body = buf.toByteArray();
}
ex.sendResponseHeaders(status, body.length);Accept-Encoding 을 안 보내는 클라이언트에는 압축을 적용하면 안 되므로, 이 검사가 빠지면 구형 클라이언트가 응답을 해석 못 합니다.
static/js/plugin/vue.global.prod.js 를 static/js/plugin/vue-3.5.13/vue.global.prod.js 로 옮기고, index.html 의 <script src> 경로를 갱신하세요. 버전을 올릴 때는 어떤 절차가 필요한지 설명하세요.
<!-- index.html -->
<script src="/js/plugin/vue-3.5.13/vue.global.prod.js"></script>새 버전을 받으면 plugin/vue-3.5.14/ 폴더를 새로 만들고 index.html 의 경로만 바꿉니다. 경로 자체가 달라지므로 브라우저가 옛 캐시를 참조할 일이 없고, 문제가 생기면 경로를 되돌려 즉시 롤백할 수 있습니다.