๐ Today I Learned 24ํ์ฐจ - Gemini AI ์ฑํ ๋ด ๋ง๋ค๊ธฐ ๐ค
์ค๋์ Gemini AI๋ฅผ ํ์ฉํ ๋๋ง์ ์ฑํ ๋ด ์๋น์ค๋ฅผ ๋ง๋ค์ด๋ดค์ต๋๋ค! ๐ค HTTP ์์ฒญ๊ณผ REST API๋ฅผ ์ด์ฉํ AI ํต์ , Isar NoSQL ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ํ์ฉํ ์ฑํ ๊ธฐ๋ก ๊ด๋ฆฌ, ๊ทธ๋ฆฌ๊ณ StreamBuilder๋ฅผ ์ฌ์ฉํ ์ค์๊ฐ UI ์ ๋ฐ์ดํธ๊น์ง ์ค์ ํ๋ก์ ํธ๋ฅผ ํตํด ๋ฐฐ์๋ณด์์ด์!
๐ฏ ํ์ต ๋ชฉํ
- Gemini API๋ฅผ ์ฌ์ฉํ AI ์ฑํ ๋ฉ์์ง ์ก์์ ๊ตฌํํ๊ธฐ
- Isar NoSQL ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ก ์ฑํ ๊ธฐ๋ก ์ ์ฅ ๋ฐ ์กฐํํ๊ธฐ
- StreamBuilder๋ฅผ ํ์ฉํ ์ค์๊ฐ UI ์ ๋ฐ์ดํธ ๊ตฌํํ๊ธฐ
- HTTP ์์ฒญ๊ณผ REST API ๊ฐ๋ ์ดํดํ๊ณ ์ค์ตํ๊ธฐ
- ListView์ ์๋ ์คํฌ๋กค ์ ๋๋ฉ์ด์ ์ ์ด ๋ฐฉ๋ฒ ์ตํ๊ธฐ
๐ ํ๋ก์ ํธ ๊ฐ์
๐ก ๊ตฌํ ๊ธฐ๋ฅ
| ๊ธฐ๋ฅ | ์ค๋ช | ์ฌ์ฉ ๊ธฐ์ |
|---|---|---|
| AI ์ฑํ | Gemini API๋ฅผ ํตํ ์ค์๊ฐ ๋ํ | REST API, HTTP ์์ฒญ |
| ๋ฐ์ดํฐ ์ ์ฅ | ์ฑํ ๊ธฐ๋ก์ ๋ก์ปฌ์ ์๊ตฌ ๋ณด๊ด | Isar NoSQL Database |
| ์ค์๊ฐ UI | ๋ฉ์์ง ๋์ฐฉ ์ ์๋ ํ๋ฉด ๊ฐฑ์ | StreamBuilder |
| ์๋ ์คํฌ๋กค | ์ ๋ฉ์์ง ์์ ์ ์๋ ์คํฌ๋กค | ScrollController |
๐๏ธ ํ๋ก์ ํธ ์ํคํ ์ฒ
์ฌ์ฉ์ ์
๋ ฅ (TextField)
โ
โผ
์ฑํ
๋ฉ์์ง ์ ์ก (HTTP POST)
โ
โผ
Gemini API ์๋ต (Stream)
โ
โโโถ ์ค์๊ฐ UI ์
๋ฐ์ดํธ (StreamBuilder)
โ
โผ
Isar DB ์ ์ฅ
โ
โผ
์ฑํ
๊ธฐ๋ก ์กฐํ ๋ฐ ํ์ (ListView)
๐ ์ฃผ์ ๋ด์ฉ
โญ HTTP ์์ฒญ์ ์ดํด
HTTP(HyperText Transfer Protocol)๋ ํด๋ผ์ด์ธํธ์ ์๋ฒ ๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ๊ธฐ ์ํ ํต์ ๊ท์ฝ์ ๋๋ค.
๐ HTTP URL ๊ตฌ์กฐ ๋ถ์
http://www.domain.com:1234/path/to/resource?a=b&x=y
| ๊ตฌ์ฑ ์์ | ์์ | ์ค๋ช |
|---|---|---|
| ํ๋กํ ์ฝ | http:// |
ํต์ ๋ฐฉ์ (http ๋๋ https) |
| ํธ์คํธ | www.domain.com |
์์ฒญํ๋ ์๋ฒ์ ๋๋ฉ์ธ ์ฃผ์ |
| ํฌํธ | :1234 |
์๋ฒ ๋ด ํน์ ํฌํธ ๋ฒํธ - HTTP: ๊ธฐ๋ณธ 80 - HTTPS: ๊ธฐ๋ณธ 443 |
| ๊ฒฝ๋ก | /path/to/resource |
์๋ฒ ๋ด ๋ฆฌ์์ค ์์น (API ์๋ํฌ์ธํธ) |
| ์ฟผ๋ฆฌ | ?a=b&x=y |
์ถ๊ฐ ์ ๋ฌ ์ ๋ณด - ?๋ก ์์- &๋ก ๊ตฌ๋ถ- key=value ํ์ |
๐ HTTP ํค๋ (Header)
ํค๋๋ HTTP ์์ฒญ/์๋ต์ ๋ํ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ๋ด๋ ์์ญ์ ๋๋ค.
// ํค๋ ์์
headers: {
'Content-Type': 'application/json', // ๋ฐ์ดํฐ ํ์
'Authorization': 'Bearer $token', // ์ธ์ฆ ํ ํฐ
'User-Agent': 'Flutter/3.0', // ํด๋ผ์ด์ธํธ ์ ๋ณด
'Content-Length': '1234', // ๋ฐ์ดํฐ ํฌ๊ธฐ
}
๐ก ํต์ฌ: ํค๋๋ ๋ฐ์ดํฐ ํ์, ์ธ์ฆ ์ ๋ณด, ํด๋ผ์ด์ธํธ ์ ๋ณด ๋ฑ์ ์ ๋ฌํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค!
๐ HTTP API ๋ฐฉ์์ ์ข ๋ฅ
| ๋ฐฉ์ | ์ค๋ช | ํน์ง |
|---|---|---|
| REST API | ์์ ์ค์ฌ์ ํ์ค HTTP ๋ฐฉ์ | ๊ฐ๋จํ๊ณ ๋ฒ์ฉ์ , ๊ฐ์ฅ ๋๋ฆฌ ์ฌ์ฉ๋จ |
| GraphQL | ์ฟผ๋ฆฌ ์ธ์ด ๊ธฐ๋ฐ API | ํ์ํ ๋ฐ์ดํฐ๋ง ์ ํ์ ์ผ๋ก ์์ฒญ ๊ฐ๋ฅ |
| gRPC | ๊ตฌ๊ธ์ ๊ณ ์ฑ๋ฅ RPC ํ๋ ์์ํฌ | ๋น ๋ฅด๊ณ ํจ์จ์ ์ด์ง๋ง ๋ณต์กํจ |
โก REST API
REST(Representational State Transfer) API๋ HTTP๋ฅผ ํจ์จ์ ์ผ๋ก ์ฌ์ฉํ๊ธฐ ์ํ ์ํคํ ์ฒ ์คํ์ผ์ ๋๋ค.
๐ฏ REST API์ 4๊ฐ์ง ํต์ฌ ์์น
| ์์น | ์ค๋ช | ์์ |
|---|---|---|
| ๊ท ์ผํ ์ธํฐํ์ด์ค | ์ผ๊ด๋ URL ๊ตฌ์กฐ์ HTTP ๋ฉ์๋ ์ฌ์ฉ | GET /users/1, POST /users |
| ๋ฌด์ํ(Stateless) | ๊ฐ ์์ฒญ์ ๋ ๋ฆฝ์ ์ด๋ฉฐ ์๋ฒ๋ ์ํ๋ฅผ ์ ์ฅํ์ง ์์ | ๋งค ์์ฒญ๋ง๋ค ์ธ์ฆ ํ ํฐ ์ ์ก |
| ๊ณ์ธตํ ์์คํ | ํด๋ผ์ด์ธํธ-์๋ฒ ๊ฐ ์ค๊ฐ ๊ณ์ธต ํ์ฉ | ๋ก๋๋ฐธ๋ฐ์, ์บ์ ์๋ฒ ๋ฑ |
| ์บ์ ๊ฐ๋ฅ | ์๋ต ๋ฐ์ดํฐ๋ฅผ ์บ์ฑํ์ฌ ์ฑ๋ฅ ํฅ์ | Cache-Control ํค๋ ํ์ฉ |
๐ HTTP ๋ฉ์๋์ REST API
| ๋ฉ์๋ | ์ฉ๋ | ์ค๋ช |
|---|---|---|
| GET | ์กฐํ | ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ๋ ์ฌ์ฉ (Read) |
| POST | ์์ฑ | ์๋ก์ด ๋ฐ์ดํฐ๋ฅผ ์์ฑํ ๋ (Create) |
| PUT | ์ ์ฒด ์์ | ๊ธฐ์กด ๋ฐ์ดํฐ๋ฅผ ์์ ํ ๊ต์ฒด (Update) |
| PATCH | ๋ถ๋ถ ์์ | ๋ฐ์ดํฐ์ ์ผ๋ถ๋ง ์์ (Update) |
| DELETE | ์ญ์ | ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ ๋ (Delete) |
๐ง Flutter์์ Dio ํจํค์ง ์ฌ์ฉํ๊ธฐ
Dio๋ Flutter์์ ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉ๋๋ HTTP ํด๋ผ์ด์ธํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค.
// dio ํจํค์ง import
import 'package:dio/dio.dart';
void main() async {
final dio = Dio();
// GET ์์ฒญ - ๋ฐ์ดํฐ ์กฐํ
final getResp = await dio.get('https://api.example.com/users');
// POST ์์ฒญ - ๋ฐ์ดํฐ ์์ฑ
final postResp = await dio.post(
'https://api.example.com/users',
data: {'name': 'John', 'age': 30},
);
// PUT ์์ฒญ - ๋ฐ์ดํฐ ์ ์ฒด ์์
final putResp = await dio.put(
'https://api.example.com/users/1',
data: {'name': 'John Updated', 'age': 31},
);
// DELETE ์์ฒญ - ๋ฐ์ดํฐ ์ญ์
final deleteResp = await dio.delete('https://api.example.com/users/1');
}
๐ค Gemini API ์์ฒญ ์์
// Gemini API ํธ์ถ ์์
final String apiKey = 'YOUR_API_KEY';
final String baseUrl = 'https://generativelanguage.googleapis.com/v1beta';
final response = await dio.post(
'$baseUrl/models/gemini-1.5-flash:generateContent?key=$apiKey',
data: {
'contents': [
{
'parts': [
{'text': '์๋
ํ์ธ์! Flutter์ ๋ํด ์ค๋ช
ํด์ฃผ์ธ์.'}
]
}
]
},
);
| ํญ๋ชฉ | ๊ฐ |
|---|---|
| ์์ฒญ URL | https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent |
| Query | key=$API_KEY |
| Method | POST |
| Content-Type | application/json |
๐พ Isar ๋ฐ์ดํฐ๋ฒ ์ด์ค
Isar๋ Flutter์ ์ต์ ํ๋ ์ด๊ณ ์ NoSQL ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋๋ค.
๐ SQL vs NoSQL ๋น๊ต
| ๊ตฌ๋ถ | SQL | NoSQL |
|---|---|---|
| ๊ตฌ์กฐ | ์๊ฒฉํ ์คํค๋ง (ํ ์ด๋ธ, ์ปฌ๋ผ) | ์ ์ฐํ ๊ตฌ์กฐ (์ปฌ๋ ์ , ๋ฌธ์) |
| ์ฅ์ | ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ, ๊ด๊ณ ๊ด๋ฆฌ | ๋น ๋ฅธ ์๋, ์ ์ฐ์ฑ |
| ๋จ์ | ๋ณต์กํ ์ค์ , ์๋ ์ ํ | ๋ฌด๊ฒฐ์ฑ ๊ด๋ฆฌ ํ์ |
| ์ฃผ์ ์ฉ๋ | ์๋ฒ ๋ฐ์ดํฐ๋ฒ ์ด์ค | ๋ชจ๋ฐ์ผ ๋ก์ปฌ DB, ์บ์ |
๐ก ํต์ฌ: ์๋ฒ๋ ๋ฌด๊ฒฐ์ฑ์ด ์ค์ํ์ฌ SQL์ ์ ํธํ๊ณ , ๋ชจ๋ฐ์ผ ์ฑ์ ์ฑ๋ฅ๊ณผ ์ฌ์ฉ์ฑ์ด ์ค์ํ์ฌ NoSQL์ ๋ง์ด ์ฌ์ฉํฉ๋๋ค!
๐๏ธ Isar ์ปฌ๋ ์ ์ ์ํ๊ธฐ
Isar์์๋ ๋ฐ์ดํฐ๋ฅผ ์ปฌ๋ ์ (Collection) ๋จ์๋ก ๊ด๋ฆฌํฉ๋๋ค.
import 'package:isar/isar.dart';
// @collection ์ด๋
ธํ
์ด์
์ผ๋ก ์ปฌ๋ ์
์ ์
@collection
class MessageModel {
// Id ํ์
์ Isar์์ ์ ๊ณตํ๋ ๊ณ ์ ์๋ณ์
Id id = Isar.autoIncrement; // ์๋ ์ฆ๊ฐ ID
// ํ๋ ์ ์
bool isMine; // ๋ด๊ฐ ๋ณด๋ธ ๋ฉ์์ง์ธ์ง ์ฌ๋ถ
String message; // ๋ฉ์์ง ๋ด์ฉ
int? point; // ํฌ์ธํธ (์ ํ์ )
DateTime date; // ๋ฉ์์ง ์ ์ก ์๊ฐ
// ์์ฑ์
MessageModel({
this.point,
required this.isMine,
required this.message,
required this.date,
});
}
โ๏ธ Build Runner ์คํ
Build Runner๋ฅผ ์คํํ์ฌ Isar๊ฐ ์ฌ์ฉํ ๋ณด์กฐ ํ์ผ์ ์์ฑํฉ๋๋ค.
# ํจํค์ง ์ค์น
flutter pub add isar isar_flutter_libs
flutter pub add dev:isar_generator
flutter pub add dev:build_runner
# Build Runner ์คํ
dart run build_runner build
# watch ๋ชจ๋๋ก ์คํ (ํ์ผ ๋ณ๊ฒฝ ์ ์๋ ์ฌ์์ฑ)
dart run build_runner watch
์คํ ํ message_model.g.dart ํ์ผ์ด ์์ฑ๋๋ฉฐ, ์ด ํ์ผ์๋ Isar๊ฐ ์ฌ์ฉํ ์คํค๋ง์ ๋ฉ์๋๊ฐ ์ ์๋์ด ์์ต๋๋ค.
๐ง Isar ์ด๊ธฐํํ๊ธฐ
import 'package:path_provider/path_provider.dart';
import 'package:isar/isar.dart';
// Isar ์ธ์คํด์ค ์ด๊ธฐํ
Future<Isar> initIsar() async {
// ์ฑ ๋ฌธ์ ๋๋ ํ ๋ฆฌ ๊ฐ์ ธ์ค๊ธฐ
final dir = await getApplicationDocumentsDirectory();
// Isar ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด๊ธฐ
final isar = await Isar.open(
[MessageModelSchema], // ์ฌ์ฉํ ์ปฌ๋ ์
์คํค๋ง
directory: dir.path, // ์ ์ฅ ๊ฒฝ๋ก
);
return isar;
}
๐ป ์ค์ต ๋ฐ ๊ตฌํ
๐๏ธ ํ๋ก์ ํธ ๊ตฌ์กฐ
lib/
โโโ main.dart # ์ฑ ์ง์
์
โโโ models/
โ โโโ message_model.dart # ๋ฉ์์ง ๋ฐ์ดํฐ ๋ชจ๋ธ
โโโ screens/
โ โโโ home_screen.dart # ์ฑํ
ํ๋ฉด
โโโ widgets/
โ โโโ logo.dart # ๋ก๊ณ ์์ ฏ
โ โโโ message.dart # ๋ฉ์์ง ์์ ฏ
โ โโโ date_divider.dart # ๋ ์ง ๊ตฌ๋ถ์
โ โโโ chat_text_field.dart # ์
๋ ฅ ํ๋
โ โโโ point_notification.dart # ํฌ์ธํธ ์๋ฆผ
โโโ services/
โโโ gemini_service.dart # Gemini API ์๋น์ค
๐ฆ 1. ์์กด์ฑ ํจํค์ง ์ค์น
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
dio: ^5.4.0 # HTTP ํด๋ผ์ด์ธํธ
isar: ^3.1.0 # NoSQL ๋ฐ์ดํฐ๋ฒ ์ด์ค
isar_flutter_libs: ^3.1.0 # Isar Flutter ๋ผ์ด๋ธ๋ฌ๋ฆฌ
path_provider: ^2.1.1 # ํ์ผ ๊ฒฝ๋ก ์ ๊ณต
get_it: ^7.6.4 # ์์กด์ฑ ์ฃผ์
dev_dependencies:
isar_generator: ^3.1.0 # Isar ์ฝ๋ ์์ฑ
build_runner: ^2.4.6 # ๋น๋ ๋๊ตฌ
๐ง 2. Isar ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธํ
GetIt์ ์ฌ์ฉํ ์์กด์ฑ ์ฃผ์
import 'package:get_it/get_it.dart';
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';
// GetIt ์ฑ๊ธํค ์ธ์คํด์ค
final getIt = GetIt.instance;
// Isar ์ด๊ธฐํ ๋ฐ ๋ฑ๋ก
Future<void> setupIsar() async {
// ์ฑ ๋ฌธ์ ๋๋ ํ ๋ฆฌ ๊ฐ์ ธ์ค๊ธฐ
final dir = await getApplicationDocumentsDirectory();
// Isar ์ธ์คํด์ค ์์ฑ
final isar = await Isar.open(
[MessageModelSchema], // ์ฌ์ฉํ ์คํค๋ง
directory: dir.path,
);
// GetIt์ ๋ฑ๋กํ์ฌ ์ด๋์๋ ์ฌ์ฉ ๊ฐ๋ฅํ๊ฒ ํจ
getIt.registerSingleton<Isar>(isar);
}
// main.dart์์ ์ด๊ธฐํ
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await setupIsar(); // Isar ์ด๊ธฐํ
runApp(MyApp());
}
๐ฑ 3. StreamBuilder๋ก ์ค์๊ฐ UI ๊ตฌํ
StreamBuilder๋ Stream ๋ฐ์ดํฐ๋ฅผ ์ค์๊ฐ์ผ๋ก ๊ฐ์งํ์ฌ UI๋ฅผ ์๋์ผ๋ก ์ ๋ฐ์ดํธํฉ๋๋ค.
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
class HomeScreen extends StatefulWidget {
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final ScrollController scrollController = ScrollController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Gemini ์ฑํ
๋ด')),
body: Column(
children: [
// ๋ฉ์์ง ๋ฆฌ์คํธ (StreamBuilder ์ฌ์ฉ)
Expanded(
child: StreamBuilder<List<MessageModel>>(
// Isar์ watch()๋ก ๋ฐ์ดํฐ ๋ณ๊ฒฝ ๊ฐ์ง
stream: GetIt.I<Isar>()
.messageModels
.where()
.watch(fireImmediately: true), // ์ฆ์ ์คํ
builder: (context, snapshot) {
// ๋ก๋ฉ ์ค์ผ ๋
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
// ๋ฉ์์ง ๋ฐ์ดํฐ
final messages = snapshot.data ?? [];
// ๋ฉ์์ง๊ฐ ์์ ๋
if (messages.isEmpty) {
return Center(child: Text('๋ฉ์์ง๊ฐ ์์ต๋๋ค'));
}
// ๋ฉ์์ง ๋ฆฌ์คํธ ๋ ๋๋ง
return ListView.builder(
controller: scrollController,
itemCount: messages.length,
itemBuilder: (context, index) {
final message = messages[index];
return MessageWidget(message: message);
},
);
},
),
),
// ์
๋ ฅ ํ๋
ChatTextField(
onSendMessage: (text) async {
await sendMessage(text);
scrollToBottom(); // ๋ฉ์์ง ์ ์ก ํ ์คํฌ๋กค
},
),
],
),
);
}
// ๋ฉ์์ง ์ ์ก ํจ์
Future<void> sendMessage(String text) async {
final isar = GetIt.I<Isar>();
// ๋ด ๋ฉ์์ง ์ ์ฅ
await isar.writeTxn(() async {
await isar.messageModels.put(MessageModel(
isMine: true,
message: text,
date: DateTime.now(),
));
});
// Gemini API ํธ์ถ ๋ฐ ์๋ต ์ ์ฅ
// (์ค์ ๊ตฌํ์ GeminiService์์)
}
// ์๋๋ก ์คํฌ๋กค
void scrollToBottom() {
if (scrollController.hasClients) {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
}
}
โก 4. ์๋ ์คํฌ๋กค ์ ๋๋ฉ์ด์
WidgetsBinding.addPostFrameCallback์ ์ฌ์ฉํ์ฌ ์์ ฏ ๋ ๋๋ง ์งํ ์คํฌ๋กค์ ์คํํฉ๋๋ค.
class _HomeScreenState extends State<HomeScreen> {
final ScrollController scrollController = ScrollController();
@override
Widget build(BuildContext context) {
return StreamBuilder<List<MessageModel>>(
stream: GetIt.I<Isar>().messageModels.where().watch(fireImmediately: true),
builder: (context, snapshot) {
final messages = snapshot.data ?? [];
// ํ๋ ์ ๋ ๋๋ง ํ ์คํฌ๋กค ์คํ (๋ฑ ํ ๋ฒ๋ง!)
WidgetsBinding.instance.addPostFrameCallback((_) {
scrollToBottom();
});
return buildMessageList(messages);
},
);
}
// ์คํฌ๋กค ์ ๋๋ฉ์ด์
Future<void> scrollToBottom() async {
if (!scrollController.hasClients) return;
await scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
}
๐ก ํต์ฌ ํฌ์ธํธ:
addPostFrameCallback์ ํ์ฌ ํ๋ ์์ด ๋ ๋๋ง๋ ์งํ ๋ฑ ํ ๋ฒ๋ง ์คํ๋ฉ๋๋ค.build()๋ ์์ฃผ ํธ์ถ๋๋ฏ๋ก, ๋งค๋ฒ ๋ฌด๊ฑฐ์ด ์์ ์ ์คํํ๋ฉด ์ฑ๋ฅ์ด ์ ํ๋ฉ๋๋ค!
๐จ 5. ์ฃผ์ ์์ ฏ ๊ตฌํ ์ฒดํฌ๋ฆฌ์คํธ
- โ Logo ์์ ฏ: ์ฑ ์๋จ ๋ก๊ณ ํ์
- โ Message ์์ ฏ: ๋ฉ์์ง ๋งํ์ (๋ด ๋ฉ์์ง / ์๋ ๋ฉ์์ง ๊ตฌ๋ถ)
- โ DateDivider ์์ ฏ: ๋ ์ง๋ณ ๊ตฌ๋ถ์
- โ ChatTextField ์์ ฏ: ๋ฉ์์ง ์ ๋ ฅ ํ๋์ ์ ์ก ๋ฒํผ
- โ PointNotification ์์ ฏ: ํฌ์ธํธ ํ๋ ์๋ฆผ
๐ ๋ง๋ฌด๋ฆฌ
โ ์ค๋ ๋ฐฐ์ด ๊ฒ
- HTTP & REST API: URL ๊ตฌ์กฐ, HTTP ๋ฉ์๋, Dio๋ฅผ ์ฌ์ฉํ API ํธ์ถ ๋ฐฉ๋ฒ
- Gemini AI ์ฐ๋: REST API๋ฅผ ํตํ AI ์ฑํ ๋ฉ์์ง ์ก์์ ๊ตฌํ
- Isar NoSQL: ์ปฌ๋ ์ ์ ์, Build Runner ์ฌ์ฉ๋ฒ, ๋ฐ์ดํฐ CRUD ์์
- StreamBuilder: ์ค์๊ฐ ๋ฐ์ดํฐ ๋ณ๊ฒฝ ๊ฐ์ง ๋ฐ ์๋ UI ์ ๋ฐ์ดํธ
- ์๋ ์คํฌ๋กค:
addPostFrameCallback์ ํ์ฉํ ํจ์จ์ ์ธ ์คํฌ๋กค ์ ์ด - ์์กด์ฑ ์ฃผ์ : GetIt์ ์ฌ์ฉํ ์ ์ญ ์ํ ๊ด๋ฆฌ
๐ ๋ค์ ๊ณํ
- ์คํธ๋ฆผ ์ฌํ:
Stream,StreamController์ง์ ๋ค๋ฃจ๊ธฐ - ์๋ฌ ์ฒ๋ฆฌ: API ํธ์ถ ์คํจ ์ ์ฌ์๋ ๋ก์ง ๊ตฌํ
- ์คํ๋ผ์ธ ๋ชจ๋: ๋คํธ์ํฌ ์์ ๋ ๋ก์ปฌ DB๋ง์ผ๋ก ๋์ํ๊ธฐ
- ์ฑ๋ฅ ์ต์ ํ: ๋ฉ์์ง ํ์ด์ง, ์ด๋ฏธ์ง ์บ์ฑ ์ ์ฉ
- ๊ณ ๊ธ ๊ธฐ๋ฅ: ์์ฑ ์ ๋ ฅ, ๋งํฌ๋ค์ด ์ง์, ์ฝ๋ ํ์ด๋ผ์ดํ
๋๊ธ๋จ๊ธฐ๊ธฐ