* oauth fix
- white list 추가
- 배포를고려한 redirectURL 수정
async process(req: Request, res: Response, paramMap: Map<string, string>, model: Map<string, object>) {
const clientId: string = process.env.GITHUB_CLIENT_ID;
const clientSecret: string = process.env.GITHUB_CLIENT_SECRET;
const host = req.headers.get('host');
const path = '/auth/github/callback';
const fullUrl = `${PROTOCOL.HTTP}://${host}${path}`;
const redirectURI = fullUrl;
const { code } = req.query;
const tokenResponse = await axios.post('https://github.com/login/oauth/access_token', {
client_id: clientId,
client_secret: clientSecret,
code,
redirect_uri: redirectURI,
}, {
- 깃허브 설정 수정
- localhost -> 실제 ip
- 이래야 localhost로 안가고 운영서버로 콜백요청이 감
* 이미지 업로드 삽질...
- post 컬럼추가
@Column({
nullable:true,
})
image?: string;
- 이미지 저장할 절대경로, 상대경로 추가
import {join} from 'path';
//서버 프로젝트의 루트폴더 절대경로
export const PROJECT_ROOT_PATH = process.cwd();
export const PUBLIC_FOLDER_NAME = 'public';
//포스트 이미지들을 저장할 폴더 이름
export const POSTS_FOLDER_NAME = 'posts';
//공개폴더의 절대경로
// /{}/public
export const PUBLIC_FOLDER_PATH = join(
PROJECT_ROOT_PATH,
PUBLIC_FOLDER_NAME
)
//포스트 이미지를 저장할 폴더
/// /{}/public/posts
export const POST_IMAGE_PATH = join(
PUBLIC_FOLDER_PATH,
POSTS_FOLDER_NAME
)
// 상대경로
// /public/posts/xxx.jpg
// FE에서 요청시 localhost붙여서 요청하면됨.
export const POST_PUBLIC_IMAGE_PATH = join(
PUBLIC_FOLDER_NAME,
POSTS_FOLDER_NAME,
)
- body에 이런식으로 옴.
------WebKitFormBoundarycGsmlZXk3btPaghA
Content-Disposition: form-data; name="image"; filename="pepe.jpg"
Content-Type: image/jpeg
���� JFIF ` ` �� C
�� C
�� � �" ��
j�]�4;ms������ok�<�� ǭ��Y~��־���c�?�M�^,�U����R&�&-��)��ῆ��0��ѭt]>1�
���ǫs�t�=y5���B��j}��o�מ�����+���-PEP�� �RO�2߈��� ӕ�~-~�� �I?��~"�;� NV��|�(梒��0��CM� մ�[�Y��1,�H��}Af�;�`?���M�}'�ĝZU�c[M/Y�}��v��qI!�7A����9���#Oԭ5kX�n����e&��H�=C�@1I�S��yc�N��lḁ��$�&�E7�5��"��~x�'W�'���z�����
��2������<�O�O� =t�BhO�X�Ҿ�����qR�!�� �9|/�&���Xdq��w*?AX3� �=5�v&��M藺Nq_l��JM�ҳt�-����^�G��G�-��'�4 �o;Kx� =�k&����� ��k[��}�������"2���G��~����,�?���G�+J�� ����><�a��4b� �0����]�}^��(��#���'� j�����M
2��g� �ӝ�j�<Mw�m<�l�A�놌7Q�]��:t�T����V��A7��O� �V����~����R�f;��Tʽo�� <��W��h�;������J~����5��
Z�m�U8�d3���E*��S���M�t▊( ��( ��( �����<9�� K�R�.��h6�f��nR���FO���ϟ�R�B?���� I.���O�8���?�(���U�_c� �B?m+/�3X���S�mspo$V��W8*$�yX�K��b@����R(�2�|�(��T�UP�Q�QE UUP�Q�QE UUP�Q�QE UUP�Q�QE UUP�Q�QE UUP�Q�QE ](��(���4Q@��
------WebKitFormBoundarycGsmlZXk3btPaghA--
- buffer를 string으로 바꾸면 data 손실발생!
- 특히 img처리시 바꾸면 안됨!
import * as fs from 'fs';
// 이미지 파일을 Buffer로 읽기
const imageBuffer = fs.readFileSync('example.jpg');
// Buffer를 문자열로 변환 (데이터 손실 발생)
const imageString = imageBuffer.toString('utf8');
// 문자열을 다시 Buffer로 변환
const reconstructedBuffer = Buffer.from(imageString, 'utf8');
// 원본 Buffer와 비교
console.log('Original buffer length:', imageBuffer.length);
console.log('Reconstructed buffer length:', reconstructedBuffer.length);
console.log('Are buffers equal?', imageBuffer.equals(reconstructedBuffer));
// 손실된 Buffer를 파일로 저장
fs.writeFileSync('reconstructed.jpg', reconstructedBuffer);
// 16진수로 출력하여 차이 확인
console.log('Original buffer (hex):', imageBuffer.slice(0, 20).toString('hex'));
console.log('Reconstructed buffer (hex):', reconstructedBuffer.slice(0, 20).toString('hex'));
- 지금 내 was는 body를 toString후 저장하고있음.
- 해결 : toString전의 BufferType인 rawBody를 사용
- 위의사진에서
- \r\n\r\n으로 파싱 -> \r\n으로 다시파싱 -> 첫번째원소 == boundary
- data = \r\n\r\n이후의 data
- imgData = data에서 다시 boundary를 찾음 && 제거 => 파일의 끝
- imgData를 저장
import * as fs from 'fs';
import * as path from 'path';
import {POST_IMAGE_PATH} from "../domain/common/const/path.const";
import {randomUUID} from "node:crypto";
export class MultiPartImageExtractor {
private static CONTENT_TYPE_REGEX = /^Content-Type:\s*(.+)$/i;
private static FILENAME_REGEX = /filename="(.+)"/i;
private extractBoundaryAndData(buffer: Buffer): { boundary: string, data: Buffer } | null {
const headerEnd = buffer.indexOf('\r\n\r\n');
if (headerEnd === -1) {
console.error('올바른 multipart/form-data 형식이 아닙니다.');
return null;
}
const header = buffer.slice(0, headerEnd).toString('utf8');
const boundary = header.split('\r\n')[0]
const data = buffer.slice(headerEnd + 4); // '\r\n\r\n' 이후의 데이터
return { boundary, data };
}
async saveImage(buffer: Buffer): Promise<string | null> {
const extractedInfo = this.extractBoundaryAndData(buffer);
if (!extractedInfo) {
console.error('boundary와 데이터를 추출할 수 없습니다.');
return null;
}
const { boundary, data } = extractedInfo;
const uploadDir = POST_IMAGE_PATH;
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
const idx = data.indexOf(boundary)
const imageData = data.slice(0, idx);
const filepath = path.join(uploadDir, randomUUID()+'.png');
return new Promise((resolve, reject) => {
fs.writeFile(filepath, imageData, (err) => {
if (err) {
console.error('파일 저장 중 오류 발생:', err);
reject(err);
} else {
console.log(`이미지가 성공적으로 저장되었습니다: ${filepath}`);
resolve(data.toString('utf8'));
}
});
});
}
}
// 사용 예:
// const extractor = new MultipartImageExtractor();
// const buffer = // 여기에 multipart/form-data 형식의 Buffer 데이터
// extractor.saveImage(buf
- 현재 static serving은 src/main/views만 탐색한다.
- 파일이 없을경우, public도 탐색하도록 수정.
- 자꾸 url이 undifiend 에러가 뜬다.
try {
const response = await fetch('/upload-image', {
method: 'POST',
body: formData
});
if (response.status === 200) {
const result = await response.json();
insertImageToEditor(result.imageUrl);
} else {
console.error('Image upload failed');
alert('이미지 업로드에 실패했습니다.');
}
} catch (error) {
console.error('Error:', error);
alert('이미지 업로드 중 오류가 발생했습니다.');
}
- fetch요청에 대해 어떤 req가 생기는지 살펴보자.
POST /upload-image HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123XYZ
Content-Length: [전체 본문의 길이]
------WebKitFormBoundaryABC123XYZ
Content-Disposition: form-data; name="image"; filename="example.jpg"
Content-Type: image/jpeg
[이미지 파일의 바이너리 데이터]
------WebKitFormBoundaryABC123XYZ-
- ???
- 어딘가에서 맘대로 redirect 시키고 있다.
- 문제 : 헤더가없는 요청이 들어와서 was가 터지고있음.
- 다른 캠퍼들을 참고해보자.
- boundary를 기준으로
- 첫줄에만 header가있고
- postman에서하면 또 잘된다..
- 찾아보니까 다른 컨텐트 타입들을 하나의 req안에서 처리하기위해 만들어놓은폼이다.
- -> boundray를 찾고, 그안에서 contentType에 따른 처리를 각각 해줘야된다!
* 24.10.16. 처음부터 다시시작
- header의 모습
POST /upload-image HTTP/1.1
User-Agent: PostmanRuntime/7.42.0
Accept: */*
Cache-Control: no-cache
Postman-Token: 533ed65f-8e4e-4587-bc58-76a06981a2de
Host: localhost:3000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data; boundary=--------------------------708332700606877762347998
Content-Length: 135360
- body의 모습
----------------------------708332700606877762347998
Content-Disposition: form-data; name="txt"
hello
----------------------------708332700606877762347998
Content-Disposition: form-data; name=""; filename="pepe.jpg"
Content-Type: image/jpeg
����
- test결과 첫번째요청은 header가 오는데 2번째 요청부터는 header는 제외된채로 전송이되는 문제가 있다.
- 원인 : tcp, keep-alive로 인해 2번째 요청부터는 헤더제외 하고 요청을 보낸다!! ->
는 청크를 잘못하고있었기떄문
rawHeaders: POST /upload-image HTTP/1.1
User-Agent: PostmanRuntime/7.42.0
Accept: /
Cache-Control: no-cache
Postman-Token: 81a44f3e-0cb2-48bd-a74d-f39fdcb5ce7d
Host: localhost:3000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data; boundary=--------------------------024227012828041052730836
Content-Length: 135360
[183ac9d5-69f3-4a27-bde9-06cb965ce6c5] [POST] [/upload-image]
이미지가 성공적으로 저장되었습니다: C:\Users\Bisu\Desktop\boostCamp Membership\web-be-p2-was\web-be-p2-was\public\posts\fcd1f1b2-821e-4e31-92ef-c8b552d08e9c.png
rawHeaders:
----------------------------110236873527679473783056
Content-Disposition: form-data; name=""; filename="pepe_maga.jpg"
Content-Type: image/jpeg
두번째 요청부터는 헤더가 전송되지않는이유?
네, 좋은 질문입니다. 이 상황은 HTTP 지속 연결(persistent connection) 또는 Keep-Alive 연결과 관련이 있습니다. 두 번째 요청부터 헤더가 전송되지 않는 것처럼 보이는 이유를 설명해 드리겠습니다.
- HTTP 지속 연결: 현대의 HTTP/1.1은 기본적으로 지속 연결을 사용합니다. 이는 하나의 TCP 연결을 통해 여러 HTTP 요청과 응답을 주고받을 수 있다는 의미입니다.
- 첫 번째 요청과 두 번째 요청의 차이:
- 첫 번째 요청: 완전한 HTTP 헤더와 본문을 포함합니다.
- 두 번째 이후의 요청: 같은 연결을 재사용하므로, 클라이언트는 전체 HTTP 헤더를 다시 보내지 않고, 변경된 부분만 보낼 수 있습니다.
- 멀티파트 폼 데이터의 특성: 멀티파트 폼 데이터는 여러 부분으로 나뉘어 전송됩니다. 각 부분은 자체적인 헤더와 본문을 가집니다.
- 데이터 스트리밍: 대용량 파일 업로드의 경우, 데이터가 청크(chunk) 단위로 스트리밍될 수 있습니다. 이 경우 첫 번째 청크에만 전체 헤더가 포함되고, 이후의 청크들은 데이터만 포함할 수 있습니다.
- 해결방법1 : res에 connection close 명시 => 클라에서 연결을 끊도록함.
- 해봤는데 안된다. 이게 원인이 아닌것 같다.
- 간단한 htmlform으로 바꿔보자.
<form id="writing-form" class="writing-form" action="/post/save" method="POST" enctype="multipart/form-data">
<div class="input-form">
<div class="input-block">
<label for="title" class="input-label">제목</label>
<input
class="input-data title-input"
type="text"
id="title"
name="title"
required
placeholder="글의 제목을 입력하세요"
/>
</div>
<div class="input-block">
<label for="content" class="input-label">제목</label>
<input
class="input-data content-input"
type="text"
id="content"
name="content"
required
placeholder="글의 내용을 입력하세요"
/>
</div>
<div class="input-block">
<label for="image" class="input-label">이미지 첨부</label>
<input type="file" id="image" name="image" accept="image/*" class="image-upload">
</div>
<div class="submit-button-block">
<button type="submit" class="button-long submit">작성완료</button>
</div>
</div>
</form>
- 파싱대상 header, body
GET /post/save HTTP/1.1
User-Agent: PostmanRuntime/7.42.0
Accept: */*
Cache-Control: no-cache
Postman-Token: 4351a27f-23a9-4e27-a0c9-77ac4336c990
Host: localhost:3000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data; boundary=--------------------------068831087757975582587906
Content-Length: 12785
body: ----------------------------068831087757975582587906
Content-Disposition: form-data; name="title"
제목
----------------------------068831087757975582587906
Content-Disposition: form-data; name="content"
11
----------------------------068831087757975582587906
Content-Disposition: form-data; name=""; filename="pepe.jpg"
Content-Type: image/jpeg
����JFIF``��C
O�O�=t�BhO�X�Ҿ�����qR�!���9|/�&���Xdq��w*?AX3��=5�v&��M藺Nq_l��JM�ҳt�-����^�G��G�-��'�4 �o;Kx�=�k&����� ��k[��}�������"2���G��~����,�?���G�+J�� ����><�a��4b��0����]�}^��(��#���'� j�����M
2��g��ӝ�j�<Mw�m<�l�A�놌7Q�]��:t�T����V��A7��O��V����~����R�f;�Tʽo��<��W�h�;������J~����5��
Z�m�U8�d3��E*��S���M�t▊(��(��(�����<9��K�R�.��h6�f��nR���FO���ϟ�R�B
?���� I.���O�8���?�(���U�_c��B?m+/�3X���S�m⸮spo$V��W8*$�yX�K��b@����R⸮(�2�|�(��T�UP�Q�QEUUP�Q�QEUUP�Q�QEUUP�Q�QEUUP�Q�QEUUP�Q�QE](��(���4Q@��
----------------------------068831087757975582587906--
- 서버에 로직추가
- multipart인경우, 파싱후 req에 넣기
- file은 req.file에 넣기
if(req.headers.get('content-type')?.includes('multipart/form-data')){
const parsedFields = parseMultipartFormData(body);
processMultipartData(parsedFields, req);
console.log(parsedFields);
}
- 문제 : 버퍼를 string 으로 바꾸면 data 손실발생
- 해결 : 버퍼 그 자체로 조작 해야함.
import { Request } from './request';
import { Response } from './response';
import * as fs from "node:fs";
import * as path from "node:path";
import {randomUUID} from "node:crypto";
import {POST_IMAGE_PATH} from "../domain/common/const/path.const";
interface ParsedField {
name: string;
value: string | Buffer;
filename?: string;
contentType?: string;
}
export function parseMultipartFormData(bodyBuffer: Buffer): ParsedField[] {
const result: ParsedField[] = [];
let startIndex = 0;
let endIndex = bodyBuffer.indexOf('\r\n'.charCodeAt(0));
const boundary = bodyBuffer.slice(startIndex, endIndex).toString();
startIndex = endIndex + 2; // Move past \r\n
while (startIndex < bodyBuffer.length) {
endIndex = bodyBuffer.indexOf(boundary, startIndex);
if (endIndex === -1) break;
const partBuffer = bodyBuffer.slice(startIndex, endIndex);
const field = parsePartBuffer(partBuffer);
if (field) result.push(field);
startIndex = endIndex + boundary.length + 2; // Move past boundary and \r\n
}
return result;
}
function parsePartBuffer(partBuffer: Buffer): ParsedField | null {
const headerEndIndex = partBuffer.indexOf('\r\n\r\n');
if (headerEndIndex === -1) return null;
const headerStr = partBuffer.slice(0, headerEndIndex).toString();
const headers = parseHeaders(headerStr);
const contentStartIndex = headerEndIndex + 4; // Move past \r\n\r\n
const field: ParsedField = { name: headers.name, value: '' };
if (headers.filename) {
field.filename = headers.filename;
field.contentType = headers.contentType;
field.value = partBuffer.slice(contentStartIndex);
} else {
field.value = partBuffer.slice(contentStartIndex).toString().trim();
}
return field;
}
function parseHeaders(headerStr: string): { name: string; filename?: string; contentType?: string } {
const headers: { name: string; filename?: string; contentType?: string } = { name: '' };
const lines = headerStr.split('\r\n');
for (const line of lines) {
if (line.startsWith('Content-Disposition:')) {
const nameMatch = line.match(/name="([^"]+)"/);
if (nameMatch) headers.name = nameMatch[1];
const filenameMatch = line.match(/filename="([^"]+)"/);
if (filenameMatch) headers.filename = filenameMatch[1];
} else if (line.startsWith('Content-Type:')) {
headers.contentType = line.split(':')[1].trim();
}
}
return headers;
}
export function processMultipartData(parsedFields: ParsedField[], req: Request): void {
req.body = {};
for (const field of parsedFields) {
if (field.filename && field.contentType) {
// This is a file
req.file = {
fieldname: field.name,
originalname: field.filename,
encoding: 'binary', // 바이너리 데이터이므로 'binary'로 설정
mimetype: field.contentType,
buffer: field.value instanceof Buffer ? field.value : Buffer.from(field.value, 'binary')
};
} else {
// This is a text field
req.body[field.name] = field.value.toString();
}
}
}
export async function saveUploadedImage(req: Request): Promise<string | null> {
if (!req.file) {
console.error('No file uploaded');
return null;
}
const uploadDir = POST_IMAGE_PATH;
// 업로드 디렉토리가 존재하지 않으면 생성
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
// 파일 확장자 추출
const fileExt = path.extname(req.file.originalname);
// UUID를 사용하여 고유한 파일 이름 생성
const fileName = `${randomUUID()}${fileExt}`;
const filePath = path.join(uploadDir, fileName);
try {
// 파일 저장
// 파일 저장 (바이너리 모드 사용)
await fs.promises.writeFile(filePath, req.file.buffer, { flag: 'wx' });
console.log(`File saved successfully: ${filePath}`);
return fileName; // 저장된 파일 이름 반환
} catch (error) {
console.error('Error saving file:', error);
return null;
}
}
- 문제 : 브라우저요청시 여전히 header(url)가 없음.
- 해결 : 기존의 handleSocket은 아래와같은 문제가 있다.
private async handleSocket(socket: net.Socket) {
let buffer = Buffer.alloc(0);
let headersParsed = false;
let contentLength = 0;
let headers = '';
for await (const chunk of socket) {
buffer = Buffer.concat([buffer, chunk]);
if (!headersParsed) {
const headerEndIndex = buffer.indexOf(CRLF + CRLF);
if (headerEndIndex !== -1) {
headers = buffer.slice(0, headerEndIndex).toString();
contentLength = this.getContentLength(headers);
buffer = buffer.slice(headerEndIndex + 4);
headersParsed = true;
}
}
if (headersParsed && (buffer.length >= contentLength || contentLength === 0)) {
await this.handleRequest(headers, buffer.slice(0, contentLength), socket);
buffer = buffer.slice(contentLength);
headersParsed = false;
contentLength = 0;
}
}
}
private handleSocketOld(socket: net.Socket){
let bufferedData = Buffer.alloc(0);
let contentLength = 0;
let headers = '';
socket.on('data', (data: Buffer) => {
[bufferedData, headers, contentLength] = this.processData(data, socket, bufferedData, headers, contentLength);
});
socket.on('error', (error: Error) => {
console.error('Socket error:', error);
});
}
private processData(
data: Buffer,
socket: net.Socket,
bufferedData: Buffer,
headers: string,
contentLength: number
) : [Buffer, string, number] {
const dataStr = data.toString();
const headerEndIndex = dataStr.indexOf(CRLF + CRLF);
if (headerEndIndex !== -1) {
// 헤더의 끝을 찾은 경우, 헤더와 본문을 분리합니다.
headers = dataStr.slice(0, headerEndIndex);
contentLength = this.getContentLength(headers);
bufferedData = Buffer.concat([bufferedData, data.slice(headerEndIndex + CRLF.length * 2)]);
} else {
// 헤더의 끝을 찾지 못한 경우, 데이터를 버퍼에 추가합니다.
bufferedData = Buffer.concat([bufferedData, data]);
}
// 버퍼된 데이터의 길이가 콘텐츠 길이보다 크거나 같은 경우, 요청을 처리합니다.
if (bufferedData.length >= contentLength) {
this.handleRequest(headers, bufferedData.slice(0, contentLength), socket);
bufferedData = bufferedData.slice(contentLength);
}
// console.log("=====================================");
// console.log('bufferedData:', bufferedData, 'headers:', headers, 'contentLength:', contentLength);
return [bufferedData, headers, contentLength];
}
특징작동하는 코드 (위)작동하지 않는 코드 (아래)
데이터 읽기 방식 | for await...of 루프 | 'data' 이벤트 리스너 |
비동기 처리 | async/await 사용 | 콜백 기반 |
버퍼 관리 | 단일 누적 버퍼 | 함수 내부에서 관리 |
요청 완전성 확인 | 헤더와 본문 모두 수신 후 처리 | 각 청크마다 처리 가능 |
에러 처리 | 루프 내에서 자동 catch | 별도의 'error' 이벤트 핸들러 |
코드 복잡성 | 상대적으로 단순 | 상태 관리로 인해 복잡 |
메모리 사용 | 전체 요청을 버퍼에 저장 | 부분적으로 버퍼 사용 |
다중 요청 처리 | 순차적 처리 보장 | 동시 처리 가능성 있음 |
handleRequest 호출 시점 | 전체 요청 수신 후 | 부분 데이터로도 호출 가능 |
확장성 | 대용량 요청 처리에 제한 있음 | 스트리밍 처리 가능 |
* Not allowed to load local resource error 해결
- 원인 : /C:boost/web-p2-was로 직접요청
- 해결 : 이미지도 get 요청으로 받도록 수정
<% if (post.image) { %>
<img src="<%= imagePath.imagePath %>" alt="Post Image" class="post-image">
<% } %>
- 이게 처음에 C:boost/~~ 경로로 되있었음
- /public/posts/dlfksidkfsf.jpg로 변경
/**
* image : 이미지의 이름 (uuid)
* imageUrl : 이미지의 경로
*/
if(findPost.image){
const image = findPost.image;
const imagePath = '/'+path.join(POSTS_FOLDER_NAME_ABS, image);
model.set("imagePath", {imagePath : imagePath});
}
* 본문에 이미지 표시 구현
- post의 image이름을 db에 저장해놓았다. 123.jpg
- post-detail 페이지에서 이 이름을 이용해 요청을 보낼 경로를 만든다. /public/posts/123.png
- ejs의 img src에 넣으면 ' /public/posts/123.png '으로 서버에 다시 요청을 보낸다.
- 서버는 이미지를 static-serving해준다.
<% if (post.image) { %>
<img src="<%= imagePath.imagePath %>" alt="Post Image" class="post-image">
<% } %>
* 이전글, 다음글 구현
- post-detail controller에서 다음글, 이전글 정보도 db에서 가져온다.
const [prevPost, nextPost] = await Promise.all([
this.postRepository.createQueryBuilder('post')
.where('post.id < :id', { id: postId })
.orderBy('post.id', 'DESC')
.select(['post.id', 'post.title'])
.getOne(),
this.postRepository.createQueryBuilder('post')
.where('post.id > :id', { id: postId })
.orderBy('post.id', 'ASC')
.select(['post.id', 'post.title'])
.getOne()
]);
model.set("prevPost", prevPost);
model.set("nextPost", nextPost);
- ejs에서 동적 렌더링
<div class="post-navigation">
<div class="nav-buttons">
<% if (prevPost) { %>
<a href="/post/<%= prevPost.id %>" class="nav-button">이전글: <%= prevPost.title %></a>
<% } else { %>
<span class="nav-button disabled">이전글 없음</span>
<% } %>
<% if (nextPost) { %>
<a href="/post/<%= nextPost.id %>" class="nav-button">다음글: <%= nextPost.title %></a>
<% } else { %>
<span class="nav-button disabled">다음글 없음</span>
<% } %>
</div>
<div class="list-button">
<a href="/index" class="nav-button">목록으로</a>
</div>
</div>
- QA 도중 에러발생
- 원인 : tcp 연결끊김 (???)
* bug : post의 image값이 지워지는 현상
- ?? 머지
'JS > boostCamp' 카테고리의 다른 글
24. 10. 17. 개발일지 // mysql, 인덱싱 (1) | 2024.10.18 |
---|---|
24.10.14. 개발일지 // github oauth구현 (0) | 2024.10.14 |
24.10.11. 개발일지 // 배포, redis, 댓글, 조회수증가, 페이징, dns, 에러페이지 (3) | 2024.10.14 |
24. 10. 10. 개발일지 // 글쓰기 구현, 동적 url 매핑 , n+1 문제 (0) | 2024.10.10 |
24. 10. 9. 개발일지 // redirectAttributes, 로그인이완료된후 원래페이지 이동, db, n+1 문제 (1) | 2024.10.10 |