1. ๋ฌธ์
https://dreamhack.io/wargame/challenges/1533
*Dreamhack CTF Season 6 Round #8 (๐ฑDiv2) ์ ์ถ์
2. ํด๊ฒฐ ๊ณผ์
(1) ๋ฌธ์ ํ์ด์ง ๋ถ์
์ ์ํ๋ฉด ๋ก๊ทธ์ธ๊ณผ ํ์๊ฐ์ ํ๋ผ๋ ํ์ด์ง๊ฐ ๋ฌ๋ค.
ํ์๊ฐ์ ์ username, ์ด๋ฉ์ผ, ๋น๋ฐ๋ฒํธ๋ก ์ด๋ฃจ์ด์ง๋ค.
test/ test@gmail.com /test1234๋ก ํ์๊ฐ์ ํ ๋ค ๋ก๊ทธ์ธ์ ์งํํด๋ณด์๋ค.
๋์ ํฌ๋ ๋ฆฌ์คํธ๋ฅผ ์ถ๊ฐํ๋ผ๋ ํ์ด์ง๊ฐ ๋ฌ ๋ค, ํฌ๋ ๋ฆฌ์คํธ์ ์ ๋ชฉ๊ณผ ๋ด์ฉ, ๋ ์ง๋ฅผ ์ ๋ ฅ๋ฐ๋๋ค.
์ ๋ชฉ : ๋ด์ฉ์ผ๋ก ์ถ๋ ฅ๋๋ฉฐ, ์ฒดํฌ๋ฐ์ค๋ฅผ ํตํด ์๋ฃํ ์ผ์ ์ ๋ํด ํ์๋ฅผ ํ ์ ์๋ค.
๋ ์ง๋ฅผ ์ ๋ ฅ๋ฐ์๋๋ฐ ๋ ์ง ๋ด์ฉ์ด ์ถ๋ ฅ๋์ง๋ ์๋ ๊ฒ ๊ฐ๋ค.
(2) ์ฝ๋ ๋ถ์
ํ์ผ์ ๋ค์ด๋ก๋ํด๋ณด๋ front๋จ์ vue.js๋ฅผ ์ด์ฉํด์ ๊ตฌํ์ด ๋์๋ค.
vue.js๋ ์ฌ์ฉ์ ์ธํฐํ์ด์ค๋ฅผ ๊ฐ๋ฐํ๊ธฐ ์ํ ์๋ฐ์คํฌ๋ฆฝํธ ํ๋ก ํธ์๋ ํ๋ ์์ํฌ์ด๋ค.
์ถ์ฒ: https://pso62.tistory.com/entry/Vuejs%EB%9E%80-%EC%9E%A5%EC%A0%90%EA%B3%BC-%EB%8B%A8%EC%A0%90-%EC%A0%84%EB%A7%9D
create.sql ํ์ผ์ ํตํด admin๊ณ์ ์ด ์ถ๊ฐ๋์ด์๊ณ , ํ๋๊ทธ๋ Todo ํ ์ด๋ธ์ ์ ์ฅ๋์๋ค๋ ๊ฒ์ ์ ์ ์์๋ค.
์ฆ, ํ๋๊ทธ๋ admin ๊ณ์ ์ ์ฒซ๋ฒ์งธ ํฌ๋ ๋ด์ฉ์ด๋ค.
INSERT INTO Todo (todo_list_id, title, description, is_completed) VALUES (
1,
'flag',
'DH{sample_flag}',
1
);
..
INSERT INTO Users (username, email, password) VALUES (
'admin',
'admin@dreamhack.io',
'helloworld' -- redacted
);
INSERT INTO Todolist (user_id, name) VALUES (
1,
'admin'
);
ํ์๊ฐ์ (signup.js)
- SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค์ H3 ํ๋ ์์ํฌ ์ฌ์ฉ
- readBody: h3์์ ์ ๊ณตํ๋ ํจ์๋ก, request์ Body๊ฐ์ ์ฝ์
- ๋น๋ฐ๋ฒํธ๋ bcrpyt์ด๋ผ๋ ํด์ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ๋จ๋ฐฉํฅ ์ํธํํจ (๋ณตํธํ ๋ถ๊ฐ)
- openDatabase: SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฐ๊ฒฐ
import { readBody, createError } from 'h3';
import bcrypt from 'bcryptjs';
import { openDatabase } from '../utils/db';
export default async (req, res) => {
const db = await openDatabase();
const { username, email, password } = await readBody(req);
if (!username || !email || !password) {
throw createError({ statusCode: 400, statusMessage: 'All fields are required.' });
}
const hashedPassword = await bcrypt.hash(password, 10);
try {
await db.run('BEGIN');
const result = await db.run('INSERT INTO Users (username, email, password) VALUES (?, ?, ?)', [username, email, hashedPassword]);
await db.run('INSERT INTO Todolist (user_id, name) VALUES (?, ?)', [result.lastID, 'Default List']);
await db.run('COMMIT');
return { message: 'User registered successfully.', userId: result.lastID };
} catch (error) {
await db.run('ROLLBACK');
if (error.code === 'SQLITE_CONSTRAINT') {
throw createError({ statusCode: 409, statusMessage: 'Username or email already exists.' });
} else {
throw createError({ statusCode: 500, statusMessage: 'Could not register user.' });
}
}
}
ํ์๊ฐ์ ์ ํ๊ธฐ ์ํด์๋ username, email, password๊ฐ ๋ชจ๋ ํ์ํ๋ค. (ํ๋๋ผ๋ ์์ผ๋ฉด 400 ์๋ฌ ๋ฐ์)
username, email, ํด์๋ password๋ฅผ Users ํ ์ด๋ธ์ ์ฝ์ ํ๋ค. ์๋ก ์์ฑ๋ ์ฌ์ฉ์์ ID๋ฅผ ๊ธฐ๋ฐ์ผ๋ก Todolist ํ ์ด๋ธ์ ๊ธฐ๋ณธ ํ ์ผ ๋ชฉ๋ก์ ์ถ๊ฐํ๋ค. result.lastID๋ ์๋ก ์์ฑ๋ ์ฌ์ฉ์์ ID (์๋ณํค) ๋ก, ๊ธฐ๋ณธ ํ ์ผ ๋ชฉ๋ก๊ณผ ์ฐ๊ฒฐํ๋ ๋ฐ ์ฌ์ฉ๋๋ค.
Todolist ํ ์ด๋ธ์ user_id ์ด์ ํด๋น ์ฌ์ฉ์ ID๋ฅผ ์ฝ์ ํ๊ณ , name ์ด์๋ 'Default List'๋ผ๋ ๊ธฐ๋ณธ ๋ชฉ๋ก์ ์ถ๊ฐํฉ๋๋ค. ์ด ๊ณผ์ ์ ํตํด Users ํ ์ด๋ธ์ id์ Todolist ํ ์ด๋ธ์ user_id๊ฐ ์ฐ๊ฒฐ๋์ด, ์๋ก ์์ฑ๋ ์ฌ์ฉ์๊ฐ ์์ ์ ํ ์ผ ๋ชฉ๋ก์ ์ฝ๊ฒ ์ฐธ์กฐํ ์ ์๊ฒ๋๋ค.
-- Users
CREATE TABLE IF NOT EXISTS Users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL
);
-- TodoList
CREATE TABLE IF NOT EXISTS Todolist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
name TEXT NOT NULL,
description TEXT,
FOREIGN KEY (user_id) REFERENCES Users(id) ON DELETE CASCADE
);
๋ก๊ทธ์ธ
- JWT(JSON Web Token)๋ฅผ ์์ฑํ์ฌ ๋ฐํ
- ์ด๋ฉ์ผ์ด๋ ๋น๋ฐ๋ฒํธ ๋ ์ค ํ๋๋ผ๋ ํ๋ฆฌ๋ฉด 400์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
- email : Users ํ ์ด๋ธ์ ์กด์ฌํ์ง ์์ผ๋ฉด User not found / 404 ์๋ฌ ๋ฐ์
- password: ์ ๋ ฅ๊ฐ๊ณผ ๋น๋ฐ๋ฒํธ์ ํด์๊ฐ์ ๋น๊ตํ๊ณ ์์ผ๋ฉด Invalid password 401 ์๋ฌ ๋ฐ์
- ์ธ์ฆ์ ์ฑ๊ณตํ๋ฉด user.id์ ์ด๋ฉ์ผ์ ํ์ด๋ก๋๋ก ํฌํจํ๋ jwt ํ ํฐ์ ์์ฑํ๋ค. ์ด jwt ํ ํฐ์ ๋ง๋ฃ์๊ฐ์ 3์๊ฐ์ด๋ค.
ํ ํฐ ์์ฑ๊น์ง ์๋ฃ๋๋ฉด ๋ก๊ทธ์ธ์ด ์๋ฃ๋๊ณ ํ ํฐ์ ๋ฐํํ๋ค.
import { createError, readBody } from 'h3';
import bcrypt from 'bcryptjs';
import { openDatabase } from '../utils/db';
import jwt from 'jsonwebtoken';
const JWT_SECRET_KEY = process.env.JWT_SECRET_KEY || 'your_secret_key';
export default async (req, res) => {
const db = await openDatabase();
const { email, password } = await readBody(req);
if (!email || !password) {
throw createError({ statusCode: 400, statusMessage: 'Email and password are required.' });
}
try {
const user = await db.get('SELECT * FROM Users WHERE email = ?', [email]);
if (!user) {
throw createError({ statusCode: 404, statusMessage: 'User not found!' });
}
const passwordValid = await bcrypt.compare(password, user.password);
if (!passwordValid) {
throw createError({ statusCode: 401, statusMessage: 'Invalid password!' });
}
const token = jwt.sign(
{ userId: user.id, email: user.email },
JWT_SECRET_KEY,
{ expiresIn: '3h' }
);
return { message: 'Login successful!', token };
} catch (error) {
throw createError({ statusCode: 500, statusMessage: 'Error logging in user.' });
}
}
todo์ ๊ด๋ จ๋ ์ฝ๋๋ shareTodo.js, todo.js, todolist.js, updateTodo.js ์ด๋ ๊ฒ 4๊ฐ๊ฐ ์๋ค.
todolist ์ ๋ฐ์ดํธ : updateTodo.js
- verifyToken ํจ์๋ฅผ ํตํด request์์ ํ ํฐ์ ๊ฒ์ฆํ๊ณ ์ฌ์ฉ์ ๋ฐ์ดํฐ๋ฅผ ์ถ์ถํ๋ค.
import jwt from 'jsonwebtoken';
const JWT_SECRET_KEY = process.env.JWT_SECRET_KEY || 'your_secret_key';
export function verifyToken(req) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new Error('No token provided');
}
const token = authHeader.split(' ')[1];
return jwt.verify(token, JWT_SECRET_KEY);
}
- userData ๋ณ์์๋ ์ธ์ฆ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ค.
- body ๋ณ์์๋ body๊ฐ์ ์ฝ์ด์ ์ ์ฅํ๋ค.
- body์์ id์ value๋ฅผ ์ถ์ถํ๋ค.
- id: ์ ๋ฐ์ดํธํ Todo ํญ๋ชฉ์ ๊ณ ์ ID
- UPDATE todo SET is_completed = ? WHERE id = ?
import { readBody, createError } from 'h3';
import { openDatabase } from '../utils/db';
export default defineEventHandler(async (event) => {
const userData = verifyToken(event.req);
const db = await openDatabase();
const body = await readBody(event);
const { id, value} = body;
try {
const result = await db.run(
`UPDATE todo
SET is_completed = ?
WHERE id = ?`,
[value, id]
);
return { success: true, message: 'Todo updated successfully', id: result.lastID };
} catch (error) {
throw createError({ statusCode: 500, statusMessage: 'Database error: ' + error.message });
}
});
todolist ์กฐํ: todolist.js
- userData.userId๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์๊ฐ ์์ ํ ํ ์ผ ๋ชฉ๋ก์ ID๋ฅผ ์กฐํํ๊ณ , ํด๋น ID๋ฅผ ๊ฐ์ง Todo ํญ๋ชฉ๋ค์ ์กฐํํ๋ค
- sharedList: TodoShares ํ
์ด๋ธ์์ userData.userId์ ํด๋นํ๋ ํญ๋ชฉ์ ์กฐํํ์ฌ sharedList ๋ฐฐ์ด์ ์ ์ฅํ๋ค.
- for ๋ฃจํ๋ฅผ ํตํด sharedList๋ฅผ ์ํํ๋ฉฐ, permission_type์ด "owner" ๋๋ "shared"์ธ ํญ๋ชฉ๋ง์ ์ถ๊ฐํ๋ค.
- db.get์ ์ฌ์ฉํ์ฌ id์ ๋ง๋ Todo ํญ๋ชฉ์ ๊ฐ์ ธ์์ todoList ๋ฐฐ์ด์ ์ถ๊ฐํ๋ค.
import { verifyToken } from '../utils/auth';
import { openDatabase } from '../utils/db';
export default defineEventHandler(async (event) => {
try {
const userData = verifyToken(event.req);
const db = await openDatabase();
const todoList = await db.all('SELECT * FROM Todo where todo_list_id=(SELECT id FROM Todolist WHERE Todolist.user_id = ?)', [userData.userId]);
const sharedList = await db.all('SELECT * FROM TodoShares where user_id= ? ', [userData.userId]);
for (const shared of sharedList){
if (shared.permission_type === "owner" || shared.permission_type === "shared")
todoList.push(await db.get('SELECT * FROM Todo where id = ?',[shared.todo_id]));
};
return todoList;
} catch (error) {
return createError({
statusCode: 401,
statusMessage: 'Unauthorized: ' + error.message
});
}
});
ํ์ง๋ง share ๊ธฐ๋ฅ์ ์นํ์ด์ง ์์์ ์ค์ ๋ก ์ฐพ์ ์ ์๋ค.
index.vue
๊ทธ ์ด์ ๋ share ์ฝ๋๊ฐ ์์ง ๊ตฌํ์ค์ด๊ธฐ ๋๋ฌธ์ด๋ค (์๋ง ๋ฌธ์ ์ ์ ๋ชฉ์ด ๋ฒ์ ๋ช ์ธ ์ด์ ์ผ ๊ฒ)
<!--
under construction
<button @click="shareTodo"> Share </button>
-->
ํ์ง๋ง share ๊ธฐ๋ฅ๊ณผ ๊ด๋ จํ js ์ฝ๋๋ ๊ตฌํ๋์ด์๋ค. ๋จ์ํ html์ ์ถ๊ฐ๋์ง ์์์ ๋ฟ์ด๋ค.
์ด ๋ถ๋ถ์ ์ทจ์ฝ์ ์ด ์กด์ฌํ ๊ฐ๋ฅ์ฑ์ด ๋๋ค.
ํฌ๋ ๋ฆฌ์คํธ ๊ณต์ : shareTodo.js
- ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ todo.id์ ํด๋นํ๋ ํ ์ผ ํญ๋ชฉ์ ๊ฐ์ ธ์ todo_data์ ์ ์ฅํ๋ค.
- ๋ง์ฝ ํ ์ผ์ด ์๋ฃ๋ ์ํ(is_completed === 1)๋ผ๋ฉด, ํ ์ผ์ ๊ณต์ (share)ํ ์ ์์ผ๋ฏ๋ก ๋ฉ์์ง์ ํจ๊ป ํด๋น ํญ๋ชฉ์ ID๋ฅผ ๋ฐํํ๋ค.
- TodoShares ํ ์ด๋ธ์ ์ ํ์ ์ถ๊ฐํ์ฌ ํ ์ผ์ด ํน์ ์ฌ์ฉ์์๊ฒ ๊ณต์ ๋์์์ ์ ์ฅํ๋ค.
- todo_data.id๋ ๊ณต์ ํ ํ ์ผ์ ID์ด๊ณ , todo.target_id๋ ์ด ํ ์ผ์ด ๊ณต์ ๋ ๋์ ์ฌ์ฉ์์ ID์ด๋ค.
- permission_type ํ๋๋ 'shared'๋ก ์ค์ ๋์ด, ์ด ์ฌ์ฉ์๊ฐ ๊ณต์ ๋ ์ํ์์ ๋ํ๋ธ๋ค.
์ด๋, ๊ณต์ ์ํ๋ก ๋ณํํ๋ ๋ก์ง์์ ํ ํฐ์ ํ์ธํ์ฌ ๋ก๊ทธ์ธ ์ํ๋ง์ ํ์ธํ ๋ฟ, ํฌ๋๋ฆฌ์คํธ์ ์์ฑ์ ๋ณธ์ธ์ธ์ง๋ ํ์ธํ์ง ์๋๋ค.
์ด ๋ถ๋ถ์์ ํ์คํ ์ทจ์ฝ์ ์ ๋ฐ๊ฒฌํ์๋ค.
import { readBody, createError } from 'h3';
import { openDatabase } from '../utils/db';
export default defineEventHandler(async (event) => {
const userData = verifyToken(event.req);
const db = await openDatabase();
const body = await readBody(event);
const todo = body;
try {
const todo_data = await db.get(
'SELECT * FROM Todo WHERE id = ?', [todo.id]
);
if (todo_data.is_completed === 1) {
return { message: 'you cannot share already completed todo', id: todo_data.id}
}
const result = await db.run(
`INSERT INTO TodoShares (todo_id, user_id, permission_type) VALUES
(?, ?, ?)`,
[todo_data.id, todo.target_id, 'shared']
);
return { success: true, message: 'Todo shared successfully', id: result.lastID };
} catch (error) {
throw createError({ statusCode: 500, statusMessage: 'Database error: ' + error.message });
}
});
ํฌ๋๋ฆฌ์คํธ: todo.js
- title๊ณผ description ๋ ์ค ํ๋๋ผ๋ ๋๋ฝ๋๋ฉด 400 Bad Request ์ค๋ฅ๋ฅผ ๋ฐ์ํ๋ค.
- Todolist ํ ์ด๋ธ์์ ์ฌ์ฉ์์ user_id๋ฅผ ๊ธฐ์ค์ผ๋ก todo_list_id๋ฅผ ์กฐํํ๋ค
- ์ดํ, INSERT INTO ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ todo ํ ์ด๋ธ์ ์ ํ ์ผ ํญ๋ชฉ์ ์ถ๊ฐํ๋ค.
- title, description, startDate, dueDateFormatted ๋ฑ์ ํ ์ผ ํญ๋ชฉ์ ์ธ๋ถ ์ ๋ณด๋ฅผ ํฌํจํ๋ค.
- ์ฝ์ ๊ฒฐ๊ณผ๋ result์ ์ ์ฅ๋๋ฉฐ, ์๋ก ์ถ๊ฐ๋ ํ ์ผ ํญ๋ชฉ์ ID๋ result.lastID๋ก ํ์ธํ ์ ์๋ค.
import { readBody, createError } from 'h3';
import { openDatabase } from '../utils/db';
export default defineEventHandler(async (event) => {
const userData = verifyToken(event.req);
const db = await openDatabase();
const body = await readBody(event);
const { title, description, dueDate } = body;
if (!title || !description) {
throw createError({ statusCode: 400, statusMessage: 'Title and description are required.' });
}
const startDate = new Date().toISOString();
const dueDateFormatted = dueDate ? new Date(dueDate).toISOString() : null;
try {
const todoListId = await db.get('SELECT id FROM Todolist WHERE user_id = ?', [userData.userId]);
const result = await db.run(
`INSERT INTO todo (todo_list_id, title, description, start_date, due_date)
VALUES (?, ?, ?, ?, ?)`,
[todoListId.id, title, description, startDate, dueDateFormatted]
);
return { success: true, message: 'Todo added successfully', id: result.lastID };
} catch (error) {
throw createError({ statusCode: 500, statusMessage: 'Database error: ' + error.message });
}
});
(3) ์ต์คํ๋ก์
๊ฐ์ฅ ๋จผ์ ์๊ฐ๋ ์์ด๋์ด๋ admin ๊ณ์ ์ ํฌ๋๋ฆฌ์คํธ๋ฅผ share ์ํ๋ก ๋ฐ๊พธ๋ ์๋๋ฆฌ์ค๋ค.
์์ ๋งํ๋ฏ, share ์ํ๋ก ๋ฐ๊ฟ์ฃผ๋ shareTodo์์๋ ํด๋น ํฌ๋๋ฆฌ์คํธ์ ์์ฑ์์ธ์ง ํ์ธํ๋ ๋ก์ง์ด ์๊ธฐ ๋๋ฌธ์ด๋ค.
๋ฐ๋ผ์ ์์ ์์ฒญ์ ๋ณด๋ด์ฃผ๊ธฐ ์ํด vue ์ฝ๋๋ฅผ ๋ณด๋ฉด, api/shareTodo ์๋ํฌ์ธํธ๋ก POST ์์ฒญ์ ๋ณด๋ด์ผ ํ๋ฉฐ, ์ธ์ฆ์ ์ํด Authorization ํค๋์ ํ ํฐ(์๋ฌด ๊ณ์ ์ด๋ ์๊ดx)์ ํฌํจํด์ผ ํ๋ค.
async function shareTodo(todo) {
try {
const data = JSON.stringify(
todo
);
const token = localStorage.getItem('auth_token');
if (!token) {
throw new Error('login plz');
}
const response = await fetch('/api/shareTodo', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: data,
});
if (!response.ok) {
throw new Error('Failed to share api');
}
} catch (error) {
console.error('Error share todo:', error);
alert('Error share todo');
}
}
๋ํ, shareTodo.js์ ํตํด share์์ฒญ ๋ณธ๋ฌธ์๋ todo_id(Todo ํ ์ด๋ธ์ id)์ user_id(์ด๋ค ๊ณ์ ์ด๋ ์๊ดx, Users ํ ์ด๋ธ์ id)๊ฐ ํ์ํ๋ค๋ ๊ฒ์ ์ ์ ์๋ค.
import { readBody, createError } from 'h3';
import { openDatabase } from '../utils/db';
export default defineEventHandler(async (event) => {
const userData = verifyToken(event.req);
const db = await openDatabase();
const body = await readBody(event);
const todo = body;
try {
const todo_data = await db.get(
'SELECT * FROM Todo WHERE id = ?', [todo.id]
);
if (todo_data.is_completed === 1) {
return { message: 'you cannot share already completed todo', id: todo_data.id}
}
const result = await db.run(
`INSERT INTO TodoShares (todo_id, user_id, permission_type) VALUES
(?, ?, ?)`,
[todo_data.id, todo.target_id, 'shared']
);
return { success: true, message: 'Todo shared successfully', id: result.lastID };
} catch (error) {
throw createError({ statusCode: 500, statusMessage: 'Database error: ' + error.message });
}
});
๋ฐ๋ผ์ ์๋์ ๊ฐ์ด ์์ฒญ์ ๋ณด๋ด์ฃผ์๋ค.
admin ๊ณ์ ์ด ๊ฐ์ฅ ๋จผ์ ์์ฑ๋๊ณ , test ๊ณ์ ์ด ๊ทธ ๋ค์ ์์ฑ๋์์ผ๋ฏ๋ก test์ id๋ 2์ผ ๊ฒ์ด๊ณ admin ๊ณ์ ์ ๊ฐ์ฅ ๋จผ์ ์์ฑ๋ todo์ด๋ฏ๋ก, ์ด ํฌ๋์ id๋ 1์ผ ๊ฒ์ด๋ค.
fetch('/api/shareTodo', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
},
body: JSON.stringify({
id: 1,
target_id: 2
}),
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
์์ฒญ์ ์ฑ๊ณตํ์ง๋ง, ์ด๋ฏธ ์๋ฃ๋ ์์ฒญ์ด๋ผ๊ณ ๋ฌ๋ค.
์ฆ ํ๋๊ทธ๊ฐ ๋ด๊ฒจ์๋ ํฌ๋๊ฐ ์ด๋ฏธ ์๋ฃ ์ํ๋ก ์ฒดํฌ๊ฐ ๋ ๊ฒ์ด๋ค.
๋ฐ๋ผ์ ์๋ฃ๋ฅผ ํด์ ํ๋ ์์ฒญ์ ๋จผ์ ์ ์กํด์ฃผ์ด์ผ ํ๋ค.
์ด ์์ฒญ ๋ํ ์ฝ๋๋ฅผ ๋ถ์ํ์ฌ ์์ฒญ์ ๋ง๋ค์ด ๋ณด๋ด์ฃผ์๋ค.
fetch('/api/updateTodo', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
},
body: JSON.stringify({
id: 1,
value: false
}),
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
์๋ฃ ์ํ๋ฅผ ํด์ ํ๋ ์์ฒญ์ด ์ฑ๊ณตํ์๋ค.
๋ฐ๋ผ์ ์ดํ์ shared๋ก ๋ณํํ๋ ์์ฒญ์ ๋ณด๋ด์ค ๋ค todolist๋ฅผ ์๋ก ๊ณ ์นจํ๋ฉด ํ๋๊ทธ๋ฅผ ํ์ธํ ์ ์๋ค
3. ํ๋๊ทธ