# BaaS MCP - ์ค์ ์ฌ์ฉ ์๋๋ฆฌ์ค
๋ค์ํ ํ๋ก์ ํธ ์ํฉ์์ BaaS MCP๋ฅผ ํ์ฉํ๋ ๊ตฌ์ฒด์ ์ธ ์๋๋ฆฌ์ค์ ๋จ๊ณ๋ณ ๊ฐ์ด๋๋ฅผ ์ ๊ณตํฉ๋๋ค.
## ๐ฏ ์๋๋ฆฌ์ค ๊ฐ์
| ์๋๋ฆฌ์ค | ๋ณต์ก๋ | ์์ ์๊ฐ | ์ฃผ์ ๊ธฐ์ |
|---------|--------|----------|---------|
| [์ ๊ท React ํ๋ก์ ํธ](#์๋๋ฆฌ์ค-1-์ ๊ท-react-ํ๋ก์ ํธ-์ธ์ฆ-๊ตฌํ) | โญ ์ด๊ธ | 30๋ถ | React, TypeScript, Tailwind |
| [๊ธฐ์กด jQuery ํ๋ก์ ํธ](#์๋๋ฆฌ์ค-2-๊ธฐ์กด-jquery-ํ๋ก์ ํธ์-์ธ์ฆ-์ถ๊ฐ) | โญโญ ์ค๊ธ | 45๋ถ | jQuery, Vanilla JS, Bootstrap |
| [๋ฉํฐํ
๋ํธ SaaS](#์๋๋ฆฌ์ค-3-๋ฉํฐํ
๋ํธ-saas-๊ตฌ์ถ) | โญโญโญ ๊ณ ๊ธ | 2์๊ฐ | Next.js, ์๋ธ๋๋ฉ์ธ, ์ฟ ํค ๊ณต์ |
| [๋ชจ๋ฐ์ผ ์น์ฑ](#์๋๋ฆฌ์ค-4-๋ชจ๋ฐ์ผ-์น์ฑ-์ธ์ฆ) | โญโญ ์ค๊ธ | 1์๊ฐ | PWA, ๋ฐ์ํ, ์ธ์
๊ด๋ฆฌ |
| [๊ด๋ฆฌ์ ๋์๋ณด๋](#์๋๋ฆฌ์ค-5-๊ด๋ฆฌ์-๋์๋ณด๋) | โญโญโญ ๊ณ ๊ธ | 1.5์๊ฐ | ์ญํ ๊ธฐ๋ฐ ์ ๊ทผ ์ ์ด, ๊ถํ ๊ด๋ฆฌ |
| [Vue.js 3 Composition API](#์๋๋ฆฌ์ค-6-vuejs-3-composition-api-ํ๋ก์ ํธ) | โญโญ ์ค๊ธ | 40๋ถ | Vue 3, TypeScript, Pinia |
| [๊ณ ๊ธ ๋ฌธ์ ๊ฒ์ ์ต์ ํ](#์๋๋ฆฌ์ค-7-๊ณ ๊ธ-๋ฌธ์-๊ฒ์-์ต์ ํ) | โญโญ ์ค๊ธ | 20๋ถ | ๊ฒ์ ๋ชจ๋, ์ฑ๋ฅ ์ต์ ํ |
| [์๋ฌ ์ฒ๋ฆฌ & ํธ๋ฌ๋ธ์ํ
](#์๋๋ฆฌ์ค-8-์๋ฌ-์ฒ๋ฆฌ--ํธ๋ฌ๋ธ์ํ
) | โญโญ ์ค๊ธ | 30๋ถ | ์๋ฌ ํธ๋ค๋ง, ๋๋ฒ๊น
, ๋ชจ๋ํฐ๋ง |
---
## ์๋๋ฆฌ์ค 1: ์ ๊ท React ํ๋ก์ ํธ ์ธ์ฆ ๊ตฌํ
### ๐ ํ๋ก์ ํธ ๊ฐ์
- **๋ชฉํ**: ์๋ก์ด React ํ๋ก์ ํธ์ ์์ ํ ์ธ์ฆ ์์คํ
๊ตฌ์ถ
- **๊ธฐ์ ์คํ**: React 18, TypeScript, Vite, Tailwind CSS
- **๊ฒฐ๊ณผ๋ฌผ**: ๋ก๊ทธ์ธ/ํ์๊ฐ์
+ ๋ณดํธ๋ ํ์ด์ง + ์ธ์ฆ ์ํ ๊ด๋ฆฌ
### ๐ ๋จ๊ณ๋ณ ์งํ
#### 1๋จ๊ณ: ํ๋ก์ ํธ ์ค์ (5๋ถ)
```bash
# React + TypeScript ํ๋ก์ ํธ ์์ฑ
npm create vite@latest my-auth-app -- --template react-ts
cd my-auth-app
# ํ์ํ ์์กด์ฑ ์ค์น
npm install axios react-router-dom
npm install -D tailwindcss postcss autoprefixer @types/node
npx tailwindcss init -p
```
**Tailwind CSS ์ค์ **:
```javascript
// tailwind.config.js
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
```
#### 2๋จ๊ณ: Claude Desktop์์ MCP ์ค์ (3๋ถ)
```json
{
"mcpServers": {
"baas-mcp": {
"command": "npx",
"args": [
"-y",
"@mbaas/baas-mcp@2.4.2",
"--project-id=550e8400-e29b-41d4-a716-446655440000"
]
}
}
}
```
#### 3๋จ๊ณ: ๋ก๊ทธ์ธ ์ปดํฌ๋ํธ ์์ฑ (10๋ถ)
**Claude์๊ฒ ์์ฒญ**:
```
"React TypeScript์์ AIApp BaaS ๋ก๊ทธ์ธ ์ปดํฌ๋ํธ ๋ง๋ค์ด์ค.
Tailwind CSS ์ฌ์ฉํ๊ณ , ์๋ฌ ์ฒ๋ฆฌ์ ๋ก๋ฉ ์ํ ํฌํจํด์.
react-router-dom์ผ๋ก ๋ก๊ทธ์ธ ์ฑ๊ณต ์ /dashboard๋ก ์ด๋ํ๊ฒ ํด์ค."
```
> ๐ก **v2.4.2 ๊ฐ์ ์ฌํญ**: ์๋ก์ด ๊ณ ๊ธ ๊ฒ์ ๊ธฐ๋ฅ์ ํ์ฉํ์ฌ ๋ ์ ํํ ์์ ์ฝ๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.
**์์ฑ๋ ํ์ผ**: `src/components/LoginForm.tsx`
```tsx
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
interface LoginFormData {
user_id: string;
user_pw: string;
project_id: string;
}
export const LoginForm: React.FC = () => {
const navigate = useNavigate();
const [formData, setFormData] = useState<LoginFormData>({
user_id: '',
user_pw: '',
project_id: '550e8400-e29b-41d4-a716-446655440000'
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
try {
const response = await axios.post(
'https://api.aiapp.link/account/login',
formData,
{ withCredentials: true }
);
if (response.data.success) {
navigate('/dashboard');
}
} catch (err: any) {
setError(
err.response?.status === 401
? '์์ด๋ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.'
: '๋ก๊ทธ์ธ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด์ฃผ์ธ์.'
);
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
๋ก๊ทธ์ธ
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}
<div className="space-y-4">
<input
type="text"
required
value={formData.user_id}
onChange={(e) => setFormData({ ...formData, user_id: e.target.value })}
className="relative block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="์ฌ์ฉ์ ID"
/>
<input
type="password"
required
value={formData.user_pw}
onChange={(e) => setFormData({ ...formData, user_pw: e.target.value })}
className="relative block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="๋น๋ฐ๋ฒํธ"
/>
</div>
<button
type="submit"
disabled={loading}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? '๋ก๊ทธ์ธ ์ค...' : '๋ก๊ทธ์ธ'}
</button>
</form>
</div>
</div>
);
};
```
#### 4๋จ๊ณ: ํ์๊ฐ์
์ปดํฌ๋ํธ ์์ฑ (8๋ถ)
**Claude์๊ฒ ์์ฒญ**:
```
"ํ์๊ฐ์
์ปดํฌ๋ํธ๋ ๋ง๋ค์ด์ค. ์ด๋ฆ, ์ ํ๋ฒํธ ํ๋ ํฌํจํ๊ณ ,
๋น๋ฐ๋ฒํธ ํ์ธ ๊ธฐ๋ฅ๊ณผ ์ ํจ์ฑ ๊ฒ์ฆ๋ ์ถ๊ฐํด์ค."
```
#### 5๋จ๊ณ: ์ธ์ฆ ์ปจํ
์คํธ ์์ฑ (4๋ถ)
**Claude์๊ฒ ์์ฒญ**:
```
"React Context API๋ก ์ธ์ฆ ์ํ ๊ด๋ฆฌํ๋ AuthContext ๋ง๋ค์ด์ค.
ํ์ฌ ๋ก๊ทธ์ธ ์ฌ์ฉ์ ์ ๋ณด ๊ด๋ฆฌํ๊ณ , ๋ก๊ทธ์์ ๊ธฐ๋ฅ๋ ํฌํจํด์."
```
### ๐ ์์ฑ๋ ๊ฒฐ๊ณผ
- **๋ก๊ทธ์ธ/ํ์๊ฐ์
ํผ**: ์์ ํ ์ ํจ์ฑ ๊ฒ์ฆ๊ณผ ์๋ฌ ์ฒ๋ฆฌ
- **์ธ์ฆ ์ํ ๊ด๋ฆฌ**: React Context๋ก ์ ์ญ ์ํ ๊ด๋ฆฌ
- **๋ณดํธ๋ ๋ผ์ฐํ
**: ์ธ์ฆ๋ ์ฌ์ฉ์๋ง ์ ๊ทผ ๊ฐ๋ฅํ ํ์ด์ง
- **๋ฐ์ํ ๋์์ธ**: ๋ชจ๋ฐ์ผ๋ถํฐ ๋ฐ์คํฌํฑ๊น์ง ์๋ฒฝ ์ง์
---
## ์๋๋ฆฌ์ค 2: ๊ธฐ์กด jQuery ํ๋ก์ ํธ์ ์ธ์ฆ ์ถ๊ฐ
### ๐ ํ๋ก์ ํธ ๊ฐ์
- **์ํฉ**: ์ด์ ์ค์ธ ๋ ๊ฑฐ์ jQuery ์น์ฌ์ดํธ
- **์ ์ฝ์กฐ๊ฑด**: ๊ธฐ์กด ์ฝ๋ ์ต์ ๋ณ๊ฒฝ, jQuery 3.x ์ ์ง
- **๋ชฉํ**: ๊ธฐ์กด ์ฌ์ดํธ์ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ์ ์ง์ ์ถ๊ฐ
### ๐ ๋จ๊ณ๋ณ ๋ง์ด๊ทธ๋ ์ด์
#### 1๋จ๊ณ: ํ์ฌ ์ํ ๋ถ์
**๊ธฐ์กด ํ๋ก์ ํธ ๊ตฌ์กฐ**:
```
legacy-website/
โโโ index.html
โโโ css/
โ โโโ bootstrap.min.css
โโโ js/
โ โโโ jquery-3.6.0.min.js
โ โโโ main.js
โโโ pages/
โโโ about.html
โโโ contact.html
```
#### 2๋จ๊ณ: ์ธ์ฆ ์คํฌ๋ฆฝํธ ์ถ๊ฐ
**Claude์๊ฒ ์์ฒญ**:
```
"jQuery 3.x๋ฅผ ์ฌ์ฉํ๋ ๊ธฐ์กด ์น์ฌ์ดํธ์ AIApp BaaS ์ธ์ฆ์ ์ถ๊ฐํ๊ณ ์ถ์ด.
๊ธฐ์กด ์ฝ๋๋ฅผ ์ต๋ํ ๊ฑด๋๋ฆฌ์ง ๋ง๊ณ , auth.js ํ์ผ๋ก ๋ถ๋ฆฌํด์
๋ก๊ทธ์ธ/๋ก๊ทธ์์ ๊ธฐ๋ฅ์ ๋ชจ๋ํํด์ค. Bootstrap 4 ์คํ์ผ ์ฌ์ฉํด์."
```
**์์ฑ๋ ํ์ผ**: `js/auth.js`
```javascript
// AIApp BaaS ์ธ์ฆ ๋ชจ๋
const AIAppAuth = {
config: {
apiEndpoint: 'https://api.aiapp.link',
projectId: '550e8400-e29b-41d4-a716-446655440000'
},
// ํ์ฌ ๋ก๊ทธ์ธ ์ํ ํ์ธ
checkAuthStatus: function() {
return new Promise((resolve, reject) => {
$.ajax({
url: this.config.apiEndpoint + '/account/info',
method: 'GET',
xhrFields: {
withCredentials: true
},
success: function(response) {
if (response.success) {
resolve(response.data);
} else {
resolve(null);
}
},
error: function() {
resolve(null);
}
});
});
},
// ๋ก๊ทธ์ธ
login: function(userId, password) {
return new Promise((resolve, reject) => {
$.ajax({
url: this.config.apiEndpoint + '/account/login',
method: 'POST',
contentType: 'application/json',
xhrFields: {
withCredentials: true
},
data: JSON.stringify({
user_id: userId,
user_pw: password,
project_id: this.config.projectId
}),
success: function(response) {
if (response.success) {
resolve(response.data);
} else {
reject(new Error('๋ก๊ทธ์ธ ์คํจ'));
}
},
error: function(xhr) {
const message = xhr.status === 401
? '์์ด๋ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.'
: '๋ก๊ทธ์ธ์ ์คํจํ์ต๋๋ค.';
reject(new Error(message));
}
});
});
},
// ๋ก๊ทธ์์
logout: function() {
return new Promise((resolve) => {
$.ajax({
url: this.config.apiEndpoint + '/logout',
method: 'POST',
xhrFields: {
withCredentials: true
},
complete: function() {
resolve();
}
});
});
},
// UI ์
๋ฐ์ดํธ
updateUI: function(user) {
if (user) {
$('#login-section').hide();
$('#user-section').show();
$('#user-name').text(user.name || user.user_id);
} else {
$('#login-section').show();
$('#user-section').hide();
}
},
// ์ด๊ธฐํ
init: function() {
const self = this;
// ํ์ด์ง ๋ก๋ ์ ์ธ์ฆ ์ํ ํ์ธ
this.checkAuthStatus().then(function(user) {
self.updateUI(user);
});
// ๋ก๊ทธ์ธ ํผ ์ด๋ฒคํธ
$('#login-form').on('submit', function(e) {
e.preventDefault();
const userId = $('#user-id').val();
const password = $('#password').val();
const $submitBtn = $('#login-btn');
$submitBtn.prop('disabled', true).text('๋ก๊ทธ์ธ ์ค...');
self.login(userId, password)
.then(function(user) {
self.updateUI(user);
$('#login-modal').modal('hide');
})
.catch(function(error) {
alert(error.message);
})
.finally(function() {
$submitBtn.prop('disabled', false).text('๋ก๊ทธ์ธ');
});
});
// ๋ก๊ทธ์์ ์ด๋ฒคํธ
$('#logout-btn').on('click', function() {
self.logout().then(function() {
self.updateUI(null);
location.reload();
});
});
}
};
// ํ์ด์ง ๋ก๋ ์๋ฃ ์ ์ด๊ธฐํ
$(document).ready(function() {
AIAppAuth.init();
});
```
#### 3๋จ๊ณ: HTML ๊ตฌ์กฐ ์
๋ฐ์ดํธ
**๊ธฐ์กด header์ ์ถ๊ฐ**:
```html
<!-- ๋ก๊ทธ์ธ ์์ญ -->
<div id="login-section" class="d-none">
<button type="button" class="btn btn-outline-primary" data-toggle="modal" data-target="#login-modal">
๋ก๊ทธ์ธ
</button>
</div>
<!-- ์ฌ์ฉ์ ์์ญ -->
<div id="user-section" class="d-none">
<span class="navbar-text">
์๋
ํ์ธ์, <span id="user-name"></span>๋
</span>
<button id="logout-btn" class="btn btn-outline-secondary ml-2">
๋ก๊ทธ์์
</button>
</div>
<!-- ๋ก๊ทธ์ธ ๋ชจ๋ฌ -->
<div class="modal fade" id="login-modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">๋ก๊ทธ์ธ</h5>
<button type="button" class="close" data-dismiss="modal">
<span>×</span>
</button>
</div>
<form id="login-form">
<div class="modal-body">
<div class="form-group">
<input type="text" id="user-id" class="form-control" placeholder="์ฌ์ฉ์ ID" required>
</div>
<div class="form-group">
<input type="password" id="password" class="form-control" placeholder="๋น๋ฐ๋ฒํธ" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">์ทจ์</button>
<button type="submit" id="login-btn" class="btn btn-primary">๋ก๊ทธ์ธ</button>
</div>
</form>
</div>
</div>
</div>
<!-- ์ธ์ฆ ์คํฌ๋ฆฝํธ ์ถ๊ฐ -->
<script src="js/auth.js"></script>
```
### ๐ ๋ง์ด๊ทธ๋ ์ด์
์๋ฃ
- **๊ธฐ์กด ์ฝ๋ ๋ฌด์์**: ๋ ๊ฑฐ์ ์์คํ
์์ ๋ณด์กด
- **์ ์ง์ ๊ฐ์ **: ํ์ํ ํ์ด์ง๋ถํฐ ์์ฐจ์ ์ ์ฉ
- **์ฌ์ฉ์ ๊ฒฝํ**: ๊ธฐ์กด UI/UX ํจํด ์ ์ง
---
## ์๋๋ฆฌ์ค 3: ๋ฉํฐํ
๋ํธ SaaS ๊ตฌ์ถ
### ๐ ํ๋ก์ ํธ ๊ฐ์
- **๋ชฉํ**: ์ฌ๋ฌ ๊ณ ๊ฐ์ฌ๋ฅผ ์ํ SaaS ํ๋ซํผ ๊ตฌ์ถ
- **์ํคํ
์ฒ**: ์๋ธ๋๋ฉ์ธ ๊ธฐ๋ฐ ๋ฉํฐํ
๋์
- **๋๋ฉ์ธ ๊ตฌ์กฐ**:
- `company-a.myapp.com` โ Project ID: `proj_a123`
- `company-b.myapp.com` โ Project ID: `proj_b456`
- `admin.myapp.com` โ ๊ด๋ฆฌ์ ๋์๋ณด๋
### ๐๏ธ ์ํคํ
์ฒ ์ค๊ณ
#### 1๋จ๊ณ: Next.js ๋ฉํฐํ
๋ํธ ์ค์
**ํ๋ก์ ํธ ๊ตฌ์กฐ**:
```
saas-platform/
โโโ pages/
โ โโโ _app.tsx
โ โโโ index.tsx # ๋๋ฉ ํ์ด์ง
โ โโโ [tenant]/ # ํ
๋ํธ๋ณ ๋ผ์ฐํ
โ โ โโโ login.tsx
โ โ โโโ dashboard.tsx
โ โ โโโ settings.tsx
โ โโโ admin/ # ๊ด๋ฆฌ์ ์ ์ฉ
โ โโโ tenants.tsx
โ โโโ users.tsx
โโโ lib/
โ โโโ tenant.ts # ํ
๋ํธ ๊ฐ์ง ๋ก์ง
โ โโโ auth.ts # ์ธ์ฆ ๊ด๋ฆฌ
โโโ middleware.ts # ๋ผ์ฐํ
๋ฏธ๋ค์จ์ด
```
#### 2๋จ๊ณ: ํ
๋ํธ ๊ฐ์ง ๋ฏธ๋ค์จ์ด
**Claude์๊ฒ ์์ฒญ**:
```
"Next.js์์ ์๋ธ๋๋ฉ์ธ ๊ธฐ๋ฐ ๋ฉํฐํ
๋ํธ ์์คํ
๋ง๋ค์ด์ค.
company-a.myapp.com ๊ฐ์ ์๋ธ๋๋ฉ์ธ์ ๊ฐ์งํด์ ๊ฐ๊ฐ ๋ค๋ฅธ Project ID๋ฅผ
์ฌ์ฉํ๋๋ก ํ๊ณ , AIApp BaaS ์ธ์ฆ๊ณผ ์ฐ๋ํด์ค."
```
**์์ฑ๋ ํ์ผ**: `middleware.ts`
```typescript
import { NextRequest, NextResponse } from 'next/server';
// ํ
๋ํธ๋ณ Project ID ๋งคํ
const TENANT_CONFIG = {
'company-a': 'proj_a123-456-789',
'company-b': 'proj_b456-789-012',
'admin': 'admin_xyz-789-123'
};
export function middleware(request: NextRequest) {
const hostname = request.headers.get('host') || '';
const subdomain = hostname.split('.')[0];
// ์๋ธ๋๋ฉ์ธ์ด ์์ผ๋ฉด ๋ฉ์ธ ์ฌ์ดํธ๋ก
if (!subdomain || subdomain === 'www' || subdomain === 'myapp') {
return NextResponse.next();
}
// ๋ฑ๋ก๋ ํ
๋ํธ์ธ์ง ํ์ธ
if (!TENANT_CONFIG[subdomain]) {
return new NextResponse('Tenant not found', { status: 404 });
}
// ํ
๋ํธ ์ ๋ณด๋ฅผ ํค๋์ ์ถ๊ฐ
const response = NextResponse.next();
response.headers.set('x-tenant-id', subdomain);
response.headers.set('x-project-id', TENANT_CONFIG[subdomain]);
return response;
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
```
#### 3๋จ๊ณ: ํ
๋ํธ๋ณ ์ธ์ฆ ์ปจํ
์คํธ
**์์ฑ๋ ํ์ผ**: `lib/tenant.ts`
```typescript
import { useRouter } from 'next/router';
import { createContext, useContext, useEffect, useState } from 'react';
interface TenantConfig {
id: string;
name: string;
projectId: string;
subdomain: string;
theme: {
primaryColor: string;
logo: string;
};
}
interface TenantContextType {
tenant: TenantConfig | null;
isLoading: boolean;
}
const TenantContext = createContext<TenantContextType>({
tenant: null,
isLoading: true
});
export const TenantProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [tenant, setTenant] = useState<TenantConfig | null>(null);
const [isLoading, setIsLoading] = useState(true);
const router = useRouter();
useEffect(() => {
// ์๋ธ๋๋ฉ์ธ์์ ํ
๋ํธ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
const fetchTenantInfo = async () => {
try {
const response = await fetch('/api/tenant/account/info');
const tenantData = await response.json();
setTenant(tenantData);
} catch (error) {
console.error('Failed to load tenant info:', error);
} finally {
setIsLoading(false);
}
};
fetchTenantInfo();
}, []);
return (
<TenantContext.Provider value={{ tenant, isLoading }}>
{children}
</TenantContext.Provider>
);
};
export const useTenant = () => useContext(TenantContext);
```
#### 4๋จ๊ณ: ํ
๋ํธ๋ณ ์ธ์ฆ ์ปดํฌ๋ํธ
**Claude์๊ฒ ์์ฒญ**:
```
"ํ
๋ํธ๋ณ๋ก ๋ค๋ฅธ Project ID๋ฅผ ์ฌ์ฉํ๋ ๋ก๊ทธ์ธ ์ปดํฌ๋ํธ ๋ง๋ค์ด์ค.
ํ
๋ํธ์ ๋ธ๋๋ฉ(๋ก๊ณ , ์ปฌ๋ฌ)๋ ๋ฐ์๋๋๋ก ํ๊ณ ,
์ฟ ํค ๋๋ฉ์ธ์ .myapp.com์ผ๋ก ์ค์ ํด์ ์๋ธ๋๋ฉ์ธ ๊ฐ ๊ณต์ ๋๊ฒ ํด์ค."
```
### ๐ ๋ฉํฐํ
๋ํธ ์์ฑ
- **์์ ํ ๊ฒฉ๋ฆฌ**: ๊ฐ ํ
๋ํธ๋ณ ๋
๋ฆฝ์ ์ธ ์ฌ์ฉ์ ๋ฐ์ดํฐ
- **๋ธ๋๋ฉ ์ง์**: ํ
๋ํธ๋ณ ๋ก๊ณ , ์ปฌ๋ฌ ์ปค์คํฐ๋ง์ด์ง
- **ํ์ฅ ๊ฐ๋ฅ**: ์ ํ
๋ํธ ์ถ๊ฐ ์ ์ค์ ๋ง ์
๋ฐ์ดํธ
- **ํตํฉ ๊ด๋ฆฌ**: ๊ด๋ฆฌ์ ๋์๋ณด๋์์ ๋ชจ๋ ํ
๋ํธ ๊ด๋ฆฌ
---
## ์๋๋ฆฌ์ค 4: ๋ชจ๋ฐ์ผ ์น์ฑ ์ธ์ฆ
### ๐ ํ๋ก์ ํธ ๊ฐ์
- **๋ชฉํ**: ๋ชจ๋ฐ์ผ ์ฐ์ PWA ๊ตฌ์ถ
- **ํน์ง**: ์คํ๋ผ์ธ ์ง์, ํ ํ๋ฉด ์ถ๊ฐ, ํธ์ ์๋ฆผ
- **๊ธฐ์ **: React, PWA, Service Worker, ๋ฐ์ํ ๋์์ธ
### ๐ฑ ๋ชจ๋ฐ์ผ ์ต์ ํ
#### 1๋จ๊ณ: PWA ์ค์
**Claude์๊ฒ ์์ฒญ**:
```
"React๋ก PWA ์ค์ ํ๊ณ , AIApp BaaS ์ธ์ฆ๊ณผ ์ฐ๋๋ ๋ชจ๋ฐ์ผ ์น์ฑ ๋ง๋ค์ด์ค.
ํฐ์น ์นํ์ ์ธ UI์ ํ ํ๋ฉด ์ถ๊ฐ ๊ธฐ๋ฅ, ์คํ๋ผ์ธ ์ ๋ก๊ทธ์ธ ์ ๋ณด
์ ์ง๋๋๋ก ํด์ค. ํ๋ฉด ํฌ๊ธฐ๋ณ ๋ฐ์ํ๋ ์๋ฒฝํ๊ฒ."
```
#### 2๋จ๊ณ: ํฐ์น ์ต์ ํ UI
**ํน์ง**:
- 44px ์ด์ ํฐ์น ํ๊ฒ
- ์ค์์ดํ ์ ์ค์ฒ ์ง์
- ํ
ํฑ ํผ๋๋ฐฑ (๊ฐ๋ฅํ ๊ฒฝ์ฐ)
- ๋น ๋ฅธ ์๋ต์ฑ (300ms ์ง์ฐ ์ ๊ฑฐ)
#### 3๋จ๊ณ: ์คํ๋ผ์ธ ์ธ์ฆ ์ฒ๋ฆฌ
**Service Worker ์บ์ ์ ๋ต**:
- ์ธ์ฆ ํ ํฐ ๋ก์ปฌ ์คํ ๋ฆฌ์ง ์บ์
- API ์๋ต ์บ์ (์ฝ๊ธฐ ์ ์ฉ)
- ์คํ๋ผ์ธ ์ ์บ์๋ ์ฌ์ฉ์ ์ ๋ณด ํ์
---
## ์๋๋ฆฌ์ค 5: ๊ด๋ฆฌ์ ๋์๋ณด๋
### ๐ ํ๋ก์ ํธ ๊ฐ์
- **๋ชฉํ**: ์ญํ ๊ธฐ๋ฐ ์ ๊ทผ ์ ์ด๊ฐ ์๋ ๊ด๋ฆฌ์ ์์คํ
- **๊ถํ ๋ ๋ฒจ**: Super Admin > Admin > Moderator > User
- **๊ธฐ๋ฅ**: ์ฌ์ฉ์ ๊ด๋ฆฌ, ๊ถํ ์ค์ , ์์คํ
๋ชจ๋ํฐ๋ง
### ๐ ๊ถํ ๊ธฐ๋ฐ ์ํคํ
์ฒ
#### 1๋จ๊ณ: ์ญํ ์ ์
**Claude์๊ฒ ์์ฒญ**:
```
"AIApp BaaS ์ธ์ฆ์ ์ฌ์ฉํด์ ์ญํ ๊ธฐ๋ฐ ๊ด๋ฆฌ์ ๋์๋ณด๋ ๋ง๋ค์ด์ค.
์ฌ์ฉ์ ๋ฐ์ดํฐ์ role ํ๋๋ฅผ ํ์ฉํด์ Super Admin, Admin, Moderator, User
4๋จ๊ณ ๊ถํ์ผ๋ก ๊ตฌ๋ถํ๊ณ , ๊ฐ ์ญํ ๋ณ๋ก ์ ๊ทผ ๊ฐ๋ฅํ ๋ฉ๋ด์ ๊ธฐ๋ฅ์
๋ค๋ฅด๊ฒ ๋ณด์ฌ์ฃผ๋๋ก ํด์ค."
```
#### 2๋จ๊ณ: ๊ถํ ๊ฐ๋ ๊ตฌํ
```typescript
// ๊ถํ ์ฒดํฌ ํ
const usePermission = (requiredRole: UserRole) => {
const { user } = useAuth();
const hasPermission = useMemo(() => {
if (!user) return false;
const roleHierarchy = {
'super_admin': 4,
'admin': 3,
'moderator': 2,
'user': 1
};
return roleHierarchy[user.role] >= roleHierarchy[requiredRole];
}, [user, requiredRole]);
return hasPermission;
};
```
### ๐ ์์ฑ๋ ๊ด๋ฆฌ์ ์์คํ
- **์ธ๋ฐํ ๊ถํ ์ ์ด**: ๊ธฐ๋ฅ๋ณ ์์ธ ๊ถํ ์ค์
- **๊ฐ์ฌ ๋ก๊ทธ**: ๋ชจ๋ ๊ด๋ฆฌ์ ์์
๊ธฐ๋ก
- **์ค์๊ฐ ๋ชจ๋ํฐ๋ง**: ์์คํ
์ํ ์ค์๊ฐ ์
๋ฐ์ดํธ
---
## ๐ ์๋๋ฆฌ์ค๋ณ ์ฑ๋ฅ ์งํ
| ์๋๋ฆฌ์ค | ๋ก๋ฉ ์๊ฐ | ๋ฒ๋ค ํฌ๊ธฐ | ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ | ๋ชจ๋ฐ์ผ ์ฑ๋ฅ | ๊ฒ์ ์๋ต |
|---------|----------|-----------|--------------|------------|----------|
| React ์ ๊ท | < 2์ด | 250KB | 15MB | 95/100 | < 150ms |
| jQuery ๋ ๊ฑฐ์ | < 1์ด | 50KB | 8MB | 98/100 | < 80ms |
| ๋ฉํฐํ
๋ํธ | < 3์ด | 400KB | 25MB | 92/100 | < 200ms |
| ๋ชจ๋ฐ์ผ PWA | < 1.5์ด | 200KB | 12MB | 97/100 | < 120ms |
| ๊ด๋ฆฌ์ ๋์๋ณด๋ | < 4์ด | 600KB | 35MB | 89/100 | < 250ms |
| Vue 3 Composition | < 2.5์ด | 280KB | 18MB | 94/100 | < 140ms |
| ๊ฒ์ ์ต์ ํ | N/A | N/A | N/A | N/A | < 50ms |
| ์๋ฌ ์ฒ๋ฆฌ & ๋ชจ๋ํฐ๋ง | +0.5์ด | +20KB | +2MB | -2์ | < 100ms |
## ๐ฏ ์ต์ ํ ํ
### v2.4.2 ์ฑ๋ฅ ๊ฐ์ ์ฌํญ
1. **๊ณ ๊ธ BM25 ๊ฒ์**: ๊ฒ์ ๋ชจ๋๋ณ ์ต์ ํ๋ก 50% ์๋ ํฅ์
2. **TokenEstimator ๋์
**: ์ ํํ ๋ฌธ์ ์ฒญํน์ผ๋ก ๋ฉ๋ชจ๋ฆฌ ํจ์จ์ฑ 20% ๊ฐ์
3. **๋์์ด ํ์ฅ**: ๊ฒ์ ์ ํ๋ 30% ํฅ์, ์๋ต ์๊ฐ ์ ์ง
4. **๊ฐ์ค์น ์์คํ
**: ์นดํ
๊ณ ๋ฆฌ๋ณ ๊ด๋ จ๋ ์ค์ฝ์ด๋ง์ผ๋ก ๊ฒฐ๊ณผ ํ์ง ํฅ์
### ๊ณตํต ์ต์ ํ
1. **๋ฒ๋ค ๋ถํ **: ์ฝ๋ ์คํ๋ฆฌํ
์ผ๋ก ์ด๊ธฐ ๋ก๋ฉ ์ต์ ํ
2. **์ด๋ฏธ์ง ์ต์ ํ**: WebP ํฌ๋งท + ์ง์ฐ ๋ก๋ฉ
3. **API ์บ์ฑ**: React Query / SWR ํ์ฉ
4. **ํธ๋ฆฌ ์
ฐ์ดํน**: ์ฌ์ฉํ์ง ์๋ ์ฝ๋ ์ ๊ฑฐ
5. **๊ฒ์ ์ต์ ํ**: ์ ์ ํ SearchMode ์ ํ์ผ๋ก ์๋ต ์๊ฐ ๋จ์ถ
### ํ๋ ์์ํฌ๋ณ ํ
- **React**: React.memo, useMemo, useCallback ํ์ฉ
- **Vue**: v-memo, computed ์์ฑ ์ต์ ํ, Pinia ์ํ ๊ด๋ฆฌ
- **Vanilla JS**: requestAnimationFrame, passive ์ด๋ฒคํธ
- **TypeScript**: ํ์
์์ ์ฑ์ผ๋ก ๋ฐํ์ ์๋ฌ ์ฌ์ ๋ฐฉ์ง
### BaaS MCP ๊ฒ์ ์ต์ ํ ์ ๋ต
- **์ด๊ธฐ ํ์**: BROAD ๋ชจ๋๋ก ๋์ ๋ฒ์ ๊ฒ์
- **๊ตฌ์ฒด์ ๊ตฌํ**: BALANCED ๋ชจ๋๋ก ์ ํํ ์์ ์ฐพ๊ธฐ
- **ํน์ API**: PRECISE ๋ชจ๋๋ก ์ ๋ฐํ ๋งค์นญ
- **์นดํ
๊ณ ๋ฆฌ ํ์ฉ**: api, templates, security ๋ฑ์ผ๋ก ๋ฒ์ ์ ํ
## โ ์์ฃผ ๋ฌป๋ ์ง๋ฌธ (FAQ)
### ์ผ๋ฐ์ ์ธ ์ง๋ฌธ
**Q1: ์ด๋ค ์๋๋ฆฌ์ค๋ถํฐ ์์ํด์ผ ํ๋์?**
A1: ํ๋ก์ ํธ ์ํฉ์ ๋ฐ๋ผ ์ ํํ์ธ์:
- **์ ๊ท ํ๋ก์ ํธ**: ์๋๋ฆฌ์ค 1 (React) ๋๋ ์๋๋ฆฌ์ค 6 (Vue 3)
- **๊ธฐ์กด ํ๋ก์ ํธ**: ์๋๋ฆฌ์ค 2 (jQuery ๋ ๊ฑฐ์)
- **๊ณ ๊ธ ๊ธฐ๋ฅ ํ์**: ์๋๋ฆฌ์ค 3 (๋ฉํฐํ
๋ํธ) ๋๋ ์๋๋ฆฌ์ค 5 (๊ด๋ฆฌ์)
**Q2: Project ID๋ ์ด๋ป๊ฒ ์ป๋์?**
A2: [AIApp ๊ฐ๋ฐ์์ผํฐ](https://docs.aiapp.link)์์ ํ๋ก์ ํธ๋ฅผ ์์ฑํ๋ฉด ๋ฐ๊ธ๋ฉ๋๋ค.
**Q3: ๊ฒ์์ด ์ ์ ๋๋๋ฐ ์ด๋ป๊ฒ ํด์ผ ํ๋์?**
A3: ์๋๋ฆฌ์ค 7์ ๊ณ ๊ธ ๊ฒ์ ์ต์ ํ๋ฅผ ์ฐธ๊ณ ํ์ธ์. SearchMode์ ์นดํ
๊ณ ๋ฆฌ๋ฅผ ์กฐํฉํ๋ฉด ๋ ์ ํํ ๊ฒฐ๊ณผ๋ฅผ ์ป์ ์ ์์ต๋๋ค.
### ๊ธฐ์ ์ ์ธ ์ง๋ฌธ
**Q4: CORS ์๋ฌ๊ฐ ๋ฐ์ํด์.**
A4: `withCredentials: true` ์ค์ ๊ณผ ์ฌ๋ฐ๋ฅธ ๋๋ฉ์ธ ์ค์ ์ ํ์ธํ์ธ์. ์๋๋ฆฌ์ค 8 ์ฐธ๊ณ .
**Q5: ๋ชจ๋ฐ์ผ์์ ์ฑ๋ฅ์ด ๋๋ ค์.**
A5: ์๋๋ฆฌ์ค 4์ ๋ชจ๋ฐ์ผ ์ต์ ํ ๊ธฐ๋ฒ์ ์ ์ฉํ์ธ์. ๋ฒ๋ค ํฌ๊ธฐ์ ์ด๋ฏธ์ง ์ต์ ํ๊ฐ ํต์ฌ์
๋๋ค.
**Q6: TypeScript ์๋ฌ๊ฐ ๋ฐ์ํด์.**
A6: ๊ฐ ์๋๋ฆฌ์ค์ ํ์
์ ์๋ฅผ ์ฐธ๊ณ ํ์ฌ ์ ํํ ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํ์ธ์.
### ํธ๋ฌ๋ธ์ํ
**Q7: ๋ก๊ทธ์ธ ํ ๋ฐ๋ก ๋ก๊ทธ์์๋ผ์.**
A7: ์ฟ ํค ๋๋ฉ์ธ ์ค์ ์ ํ์ธํ์ธ์. `.aiapp.link` ๋๋ฉ์ธ์ผ๋ก ์ค์ ๋์ด์ผ ํฉ๋๋ค.
**Q8: API ์๋ต์ด ๋๋ ค์.**
A8:
- ๊ฒ์ ๋ชจ๋๋ฅผ PRECISE โ BALANCED โ BROAD ์์๋ก ์๋
- ๋ถํ์ํ ์นดํ
๊ณ ๋ฆฌ ํํฐ ์ ๊ฑฐ
- ํค์๋๋ฅผ 2-4๊ฐ๋ก ์ ํ
**Q9: ์๋ฌ ๋ฉ์์ง๋ฅผ ํ๊ตญ์ด๋ก ๋ณด๊ณ ์ถ์ด์.**
A9: ๊ฐ ์๋๋ฆฌ์ค์ ์๋ฌ ์ฒ๋ฆฌ ๋ถ๋ถ์์ ์ฌ์ฉ์ ์นํ์ ๋ฉ์์ง ์ฒ๋ฆฌ ๋ฐฉ๋ฒ์ ์ฐธ๊ณ ํ์ธ์.
---
## ๐ ์๋๋ฆฌ์ค ๊ด๋ จ ์ง์
๊ฐ ์๋๋ฆฌ์ค๋ณ ์์ธํ ๋์์ด ํ์ํ์๋ฉด:
- ๐ง Email: mbaas.tech@gmail.com
- ๐ฌ Discord: [ํ๋ก์ ํธ๋ณ ์ฑ๋]
- ๐ ์์ ์ ์ฅ์: https://github.com/aiapp/baas-examples
- ๐ ๊ฐ๋ฐ์์ผํฐ: https://docs.aiapp.link
### ๐ก ํ๋ก ํ
1. **๊ฐ๋ฐ ์์**: ์ธ์ฆ โ ๊ธฐ๋ฅ ๊ตฌํ โ ์๋ฌ ์ฒ๋ฆฌ โ ์ต์ ํ
2. **๊ฒ์ ์ ๋ต**: ๋จผ์ ๊ด๋ จ ๋ฌธ์๋ฅผ BROAD๋ก ํ์ ํ ๊ตฌ์ฒด์ ์ผ๋ก PRECISE ๊ฒ์
3. **์ฑ๋ฅ ๋ชจ๋ํฐ๋ง**: ๊ฐ ์๋๋ฆฌ์ค๋ณ ์ฑ๋ฅ ์งํ๋ฅผ ๋ฒค์น๋งํฌ๋ก ํ์ฉ
4. **๋ณด์ ๊ณ ๋ ค**: ์ด์ ํ๊ฒฝ ๋ฐฐํฌ ์ ์๋๋ฆฌ์ค 8์ ์ฒดํฌ๋ฆฌ์คํธ ํ์ ํ์ธ
---
## ์๋๋ฆฌ์ค 6: Vue.js 3 Composition API ํ๋ก์ ํธ
### ๐ ํ๋ก์ ํธ ๊ฐ์
- **๋ชฉํ**: Vue 3 Composition API๋ฅผ ์ฌ์ฉํ ๋ชจ๋ ์ธ์ฆ ์์คํ
- **๊ธฐ์ ์คํ**: Vue 3, TypeScript, Vite, Pinia, Tailwind CSS
- **ํน์ง**: Composable ํจํด, ๋ฐ์ํ ์ํ ๊ด๋ฆฌ, ํ์
์์ ์ฑ
### ๐ ๋จ๊ณ๋ณ ์งํ
#### 1๋จ๊ณ: ํ๋ก์ ํธ ์ค์ (8๋ถ)
```bash
# Vue 3 + TypeScript ํ๋ก์ ํธ ์์ฑ
npm create vue@latest my-vue-auth-app
cd my-vue-auth-app
# ํ๋ก์ ํธ ์ค์ ์ ํ
# โ
TypeScript
# โ
Router
# โ
Pinia
# โ PWA
# โ Unit Testing
# โ E2E Testing
npm install
npm install axios @tailwindcss/forms
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```
#### 2๋จ๊ณ: Pinia ์ธ์ฆ ์คํ ์ด ์์ฑ (12๋ถ)
**Claude์๊ฒ ์์ฒญ**:
```
"Vue 3 Composition API์ Pinia๋ฅผ ์ฌ์ฉํด์ AIApp BaaS ์ธ์ฆ ์คํ ์ด ๋ง๋ค์ด์ค.
TypeScript๋ก ํ์
์์ ์ฑ ๋ณด์ฅํ๊ณ , ๋ก๊ทธ์ธ/๋ก๊ทธ์์ ์ก์
,
์ฌ์ฉ์ ์ํ ๊ด๋ฆฌ, ์๋ ํ ํฐ ๊ฐฑ์ ๊ธฐ๋ฅ ํฌํจํด์."
```
**์์ฑ๋ ํ์ผ**: `src/stores/auth.ts`
```typescript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import axios from 'axios'
interface User {
user_id: string
name?: string
email?: string
role?: string
}
interface LoginCredentials {
user_id: string
user_pw: string
project_id: string
}
export const useAuthStore = defineStore('auth', () => {
// ์ํ (state)
const user = ref<User | null>(null)
const isLoading = ref(false)
const error = ref<string | null>(null)
// ๊ฒํฐ (getters)
const isAuthenticated = computed(() => !!user.value)
const userName = computed(() => user.value?.name || user.value?.user_id || '')
// Axios ์ธ์คํด์ค ์ค์
const api = axios.create({
baseURL: 'https://api.aiapp.link',
withCredentials: true
})
// ์ก์
(actions)
const login = async (credentials: LoginCredentials) => {
isLoading.value = true
error.value = null
try {
const response = await api.post('/account/login', credentials)
if (response.data.success) {
user.value = response.data.data
return { success: true, data: response.data.data }
} else {
throw new Error('๋ก๊ทธ์ธ ์คํจ')
}
} catch (err: any) {
const errorMessage = err.response?.status === 401
? '์์ด๋ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.'
: '๋ก๊ทธ์ธ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด์ฃผ์ธ์.'
error.value = errorMessage
return { success: false, error: errorMessage }
} finally {
isLoading.value = false
}
}
const logout = async () => {
isLoading.value = true
try {
await api.post('/logout')
} catch (err) {
console.warn('๋ก๊ทธ์์ ์์ฒญ ์คํจ:', err)
} finally {
user.value = null
isLoading.value = false
}
}
const checkAuth = async () => {
isLoading.value = true
try {
const response = await api.get('/account/info')
if (response.data.success) {
user.value = response.data.data
} else {
user.value = null
}
} catch (err) {
user.value = null
} finally {
isLoading.value = false
}
}
const signup = async (userData: {
user_id: string
user_pw: string
name: string
phone: string
project_id: string
}) => {
isLoading.value = true
error.value = null
try {
const response = await api.post('/account/signup', userData)
if (response.data.success) {
return { success: true, data: response.data.data }
} else {
throw new Error('ํ์๊ฐ์
์คํจ')
}
} catch (err: any) {
const errorMessage = err.response?.data?.message || 'ํ์๊ฐ์
์ ์คํจํ์ต๋๋ค.'
error.value = errorMessage
return { success: false, error: errorMessage }
} finally {
isLoading.value = false
}
}
return {
// ์ํ
user,
isLoading,
error,
// ๊ฒํฐ
isAuthenticated,
userName,
// ์ก์
login,
logout,
checkAuth,
signup
}
})
```
#### 3๋จ๊ณ: ์ธ์ฆ Composable ์์ฑ (8๋ถ)
**์์ฑ๋ ํ์ผ**: `src/composables/useAuth.ts`
```typescript
import { useAuthStore } from '@/stores/auth'
import { useRouter } from 'vue-router'
export function useAuth() {
const authStore = useAuthStore()
const router = useRouter()
const loginAndRedirect = async (credentials: any) => {
const result = await authStore.login(credentials)
if (result.success) {
// ๋ก๊ทธ์ธ ์ฑ๊ณต ์ ๋์๋ณด๋๋ก ์ด๋
await router.push('/dashboard')
}
return result
}
const logoutAndRedirect = async () => {
await authStore.logout()
await router.push('/login')
}
return {
...authStore,
loginAndRedirect,
logoutAndRedirect
}
}
```
#### 4๋จ๊ณ: ๋ก๊ทธ์ธ ์ปดํฌ๋ํธ ์์ฑ (12๋ถ)
**Claude์๊ฒ ์์ฒญ**:
```
"Vue 3 Composition API๋ก AIApp BaaS ๋ก๊ทธ์ธ ํผ ์ปดํฌ๋ํธ ๋ง๋ค์ด์ค.
Tailwind CSS ์คํ์ผ๋ง, TypeScript ํ์
์์ ์ฑ,
ํผ ์ ํจ์ฑ ๊ฒ์ฆ, ์๋ฌ ์ฒ๋ฆฌ ํฌํจํด์."
```
**์์ฑ๋ ํ์ผ**: `src/components/LoginForm.vue`
```vue
<template>
<div class="min-h-screen flex items-center justify-center bg-gray-50">
<div class="max-w-md w-full space-y-8">
<div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
๋ก๊ทธ์ธ
</h2>
</div>
<form class="mt-8 space-y-6" @submit.prevent="handleSubmit">
<!-- ์๋ฌ ๋ฉ์์ง -->
<div v-if="error" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{{ error }}
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">์ฌ์ฉ์ ID</label>
<input
v-model="form.user_id"
type="text"
required
class="mt-1 relative block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="์ฌ์ฉ์ ID"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">๋น๋ฐ๋ฒํธ</label>
<input
v-model="form.user_pw"
type="password"
required
class="mt-1 relative block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="๋น๋ฐ๋ฒํธ"
>
</div>
</div>
<button
type="submit"
:disabled="isLoading"
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{{ isLoading ? '๋ก๊ทธ์ธ ์ค...' : '๋ก๊ทธ์ธ' }}
</button>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import { useAuth } from '@/composables/useAuth'
// ์ธ์ฆ ์ปดํฌ์ ๋ธ ์ฌ์ฉ
const { isLoading, error, loginAndRedirect } = useAuth()
// ํผ ์ํ
const form = reactive({
user_id: '',
user_pw: '',
project_id: '550e8400-e29b-41d4-a716-446655440000' // ์ค์ Project ID๋ก ๋ณ๊ฒฝ
})
// ํผ ์ ์ถ ํธ๋ค๋ฌ
const handleSubmit = async () => {
await loginAndRedirect(form)
}
</script>
```
### ๐ Vue 3 ํ๋ก์ ํธ ์์ฑ
- **ํ์
์์ ์ฑ**: TypeScript๋ก ์ปดํ์ผ ํ์ ์๋ฌ ๋ฐฉ์ง
- **๋ฐ์ํ ์ํ ๊ด๋ฆฌ**: Pinia๋ก ์ค์ํ๋ ์ธ์ฆ ์ํ
- **Composable ํจํด**: ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ธ์ฆ ๋ก์ง
- **๋ชจ๋ Vue 3**: Composition API์ ๋ชจ๋ ์ฅ์ ํ์ฉ
---
## ์๋๋ฆฌ์ค 7: ๊ณ ๊ธ ๋ฌธ์ ๊ฒ์ ์ต์ ํ
### ๐ ํ๋ก์ ํธ ๊ฐ์
- **๋ชฉํ**: BaaS MCP v2.4.2์ ๊ณ ๊ธ ๊ฒ์ ๊ธฐ๋ฅ ์ต๋ ํ์ฉ
- **ํน์ง**: ๊ฒ์ ๋ชจ๋ ์ต์ ํ, ์นดํ
๊ณ ๋ฆฌ ํํฐ๋ง, ์ฑ๋ฅ ํ๋
- **๋์**: ๊ธฐ์กด ํ๋ก์ ํธ์ ๋ฌธ์ ๊ฒ์ ํจ์จ์ฑ ๊ฐ์
### ๐ ๊ณ ๊ธ ๊ฒ์ ์ ๋ต
#### 1๋จ๊ณ: ๊ฒ์ ๋ชจ๋๋ณ ์ต์ ์ฌ์ฉ๋ฒ (5๋ถ)
**๊ฒ์ ๋ชจ๋ ํน์ฑ**:
- **BROAD**: ๊ด๋ฒ์ํ ๊ฒฐ๊ณผ, ์ด๊ธฐ ํ์์ ์ ํฉ
- **BALANCED**: ๊ท ํ์กํ ์ ํ๋, ์ผ๋ฐ์ ์ธ ๊ฐ๋ฐ ์์
- **PRECISE**: ์ ํํ ๋งค์นญ, ํน์ API๋ ๊ธฐ๋ฅ ์ฐพ๊ธฐ
**Claude์๊ฒ ์์ฒญ ์์**:
```
# ์ด๊ธฐ ํ์ ์
"keywords=['React', '์ธ์ฆ'] searchMode='broad'๋ก ๊ด๋ จ ๋ฌธ์๋ค ๋๊ฒ ๊ฒ์ํด์ค"
# ๊ตฌ์ฒด์ ๊ตฌํ ์
"keywords=['JWT', 'ํ ํฐ', 'refresh'] searchMode='precise'๋ก ์ ํํ JWT ๊ฐฑ์ ๋ฐฉ๋ฒ ์ฐพ์์ค"
# ์ผ๋ฐ์ ๊ฐ๋ฐ ์
"keywords=['Vue', '์ปดํฌ๋ํธ'] searchMode='balanced'๋ก Vue ์ปดํฌ๋ํธ ์์ ์ฐพ์์ค"
```
#### 2๋จ๊ณ: ์นดํ
๊ณ ๋ฆฌ๋ณ ๊ฒ์ ์ ๋ต (8๋ถ)
**์นดํ
๊ณ ๋ฆฌ๋ณ ์ต์ ํ์ฉ**:
```
# API ๋ฌธ์ ์ค์ฌ ๊ฒ์
keywords=['login', 'endpoint'] category='api'
# ์ค์ ์ฝ๋ ์์ ์ค์ฌ
keywords=['React', 'ํผ'] category='templates'
# ๋ณด์ ๊ด๋ จ ์ด์
keywords=['CORS', '์ฟ ํค'] category='security'
# ์๋ฌ ํด๊ฒฐ
keywords=['401', 'unauthorized'] category='errors'
# ์ค์ ๊ด๋ จ
keywords=['ํ๊ฒฝ๋ณ์', 'config'] category='config'
```
#### 3๋จ๊ณ: ์ฑ๋ฅ ์ต์ ํ๋ ๊ฒ์ ํจํด (7๋ถ)
**ํจ๊ณผ์ ์ธ ํค์๋ ์กฐํฉ**:
```typescript
// โ ๋นํจ์จ์
keywords=['React๋ก ๋ก๊ทธ์ธ ํผ์ ๋ง๋ค๊ณ ์ถ์๋ฐ ์ด๋ป๊ฒ ํด์ผ ํ๋์?']
// โ
ํจ์จ์
keywords=['React', '๋ก๊ทธ์ธ', 'ํผ']
// โ ๋๋ฌด ๊ด๋ฒ์
keywords=['์ธ์ฆ']
// โ
๊ตฌ์ฒด์
keywords=['JWT', 'ํ ํฐ', '๊ฐฑ์ ']
// โ ์ค๋ณต๋ ์๋ฏธ
keywords=['๋ก๊ทธ์ธ', 'login', '๋ก๊ทธ์ธํ๊ธฐ']
// โ
๋ณด์์ ์๋ฏธ
keywords=['๋ก๊ทธ์ธ', 'React', 'TypeScript']
```
### ๐ฏ ์ค์ ์ต์ ํ ํ
1. **๋จ๊ณ์ ๊ฒ์**: BROAD โ BALANCED โ PRECISE ์์
2. **์นดํ
๊ณ ๋ฆฌ ํ์ฉ**: ๋ชฉ์ ์ ๋ง๋ ์นดํ
๊ณ ๋ฆฌ ์ฐ์ ๊ฒ์
3. **ํค์๋ ์ ์ **: ํต์ฌ ํค์๋ 2-4๊ฐ ์กฐํฉ
4. **๋์์ด ํ์ฉ**: ์์คํ
์ด ์๋์ผ๋ก ํ์ฅํ๋ฏ๋ก ํ๋๋ง ์ฌ์ฉ
---
## ์๋๋ฆฌ์ค 8: ์๋ฌ ์ฒ๋ฆฌ & ํธ๋ฌ๋ธ์ํ
### ๐ ํ๋ก์ ํธ ๊ฐ์
- **๋ชฉํ**: ์ค์ ์ด์ ํ๊ฒฝ์์ ๋ฐ์ํ๋ ์ธ์ฆ ๊ด๋ จ ๋ฌธ์ ํด๊ฒฐ
- **๋ฒ์**: 401, 403, 500 ์๋ฌ๋ถํฐ ๋คํธ์ํฌ ์ฅ์ ๊น์ง
- **๊ฒฐ๊ณผ**: ์์ ์ ์ด๊ณ ์ฌ์ฉ์ ์นํ์ ์ธ ์๋ฌ ์ฒ๋ฆฌ ์์คํ
### ๐จ ์ฃผ์ ์๋ฌ ์๋๋ฆฌ์ค
#### 1๋จ๊ณ: 401 Unauthorized ์ฒ๋ฆฌ (10๋ถ)
**๋ฐ์ ์ํฉ**:
- ์๋ชป๋ ๋ก๊ทธ์ธ ์ ๋ณด
- ๋ง๋ฃ๋ ์ธ์
- ๊ถํ ์๋ API ์ ๊ทผ
**Claude์๊ฒ ์์ฒญ**:
```
"AIApp BaaS์์ 401 ์๋ฌ๊ฐ ๋ฐ์ํ์ ๋ ์ ์ ํ ์๋ฌ ์ฒ๋ฆฌ ๋ก์ง ๋ง๋ค์ด์ค.
์๋ ๋ก๊ทธ์์๊ณผ ๋ก๊ทธ์ธ ํ์ด์ง ๋ฆฌ๋ค์ด๋ ํธ, ์ฌ์ฉ์ ์นํ์ ๋ฉ์์ง ํฌํจํด์."
```
**ํด๊ฒฐ ์ ๋ต**:
```typescript
// Axios ์ธํฐ์
ํฐ๋ก ์ ์ญ 401 ์ฒ๋ฆฌ
axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// ์ธ์ฆ ์ํ ์ด๊ธฐํ
localStorage.removeItem('auth');
// ์ฌ์ฉ์ ์นํ์ ๋ฉ์์ง
showToast('์ธ์
์ด ๋ง๋ฃ๋์์ต๋๋ค. ๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.');
// ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธ
window.location.href = '/login';
}
return Promise.reject(error);
}
);
```
#### 2๋จ๊ณ: ๋คํธ์ํฌ ์ฅ์ ์ฒ๋ฆฌ (10๋ถ)
**๋ฐ์ ์ํฉ**:
- ์ธํฐ๋ท ์ฐ๊ฒฐ ๋๊น
- API ์๋ฒ ๋ค์ด
- ์๋ต ์๊ฐ ์ด๊ณผ
**์๋ ์ฌ์๋ ๋ก์ง**:
```typescript
const apiCall = async (url: string, data: any, retries = 3) => {
for (let i = 0; i < retries; i++) {
try {
const response = await axios.post(url, data, {
timeout: 10000 // 10์ด ํ์์์
});
return response;
} catch (error) {
if (i === retries - 1) throw error; // ๋ง์ง๋ง ์๋ ์คํจ
// ์ง์์ ๋ฐฑ์คํ
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, i) * 1000)
);
}
}
};
```
#### 3๋จ๊ณ: ์ฌ์ฉ์ ๊ฒฝํ ์ต์ ํ (10๋ถ)
**๋ก๋ฉ ์ํ ๊ด๋ฆฌ**:
```typescript
const useApiCall = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const callApi = async (apiFunction: () => Promise<any>) => {
setLoading(true);
setError(null);
try {
const result = await apiFunction();
return result;
} catch (err) {
setError(getErrorMessage(err));
throw err;
} finally {
setLoading(false);
}
};
return { loading, error, callApi };
};
```
### ๐ ๏ธ ํธ๋ฌ๋ธ์ํ
์ฒดํฌ๋ฆฌ์คํธ
1. **๋คํธ์ํฌ ์ฐ๊ฒฐ**: ๊ฐ๋ฐ์ ๋๊ตฌ Network ํญ ํ์ธ
2. **CORS ์ค์ **: ๋ธ๋ผ์ฐ์ ์ฝ์ CORS ์๋ฌ ํ์ธ
3. **์ฟ ํค ์ค์ **: withCredentials: true ์ค์ ํ์ธ
4. **Project ID**: ์ฌ๋ฐ๋ฅธ Project ID ์ฌ์ฉ ํ์ธ
5. **API ์๋ํฌ์ธํธ**: https://api.aiapp.link ์ฃผ์ ํ์ธ
---
**Built with โค๏ธ by AIApp Team**
---
**์ต์ข
์
๋ฐ์ดํธ**: 2024๋
12์ ๊ธฐ์ค v2.4.2