Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added roles/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions roles/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
6 changes: 6 additions & 0 deletions roles/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class RolesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'roles'
35 changes: 35 additions & 0 deletions roles/clova_ai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import requests
import os

CLOVA_API_URL = "https://clovastudio.stream.naver.com/testapp/v1/chat-completions/HCX"
CLOVA_API_KEY = os.environ.get("CLOVA_API_KEY")

def make_prompt(major, traits, preferences):
traits_str = ", ".join(traits)
prefs_str = ", ".join(preferences)
return f"""전공: {major}
성향: {traits_str}
선호 작업: {prefs_str}

이 정보를 바탕으로 가장 적절한 팀 역할을 하나 추천해주세요. 이유도 간단히 설명해주세요."""

def call_clova_recommendation(prompt):
headers = {
"X-NCP-CLOVASTUDIO-API-KEY": CLOVA_API_KEY,
"X-NCP-APIGW-API-KEY": CLOVA_API_KEY,
"Content-Type": "application/json",
}

payload = {
"messages": [
{"role": "system", "content": "당신은 역할을 추천하는 팀 빌딩 도우미입니다."},
{"role": "user", "content": prompt}
],
"topP": 0.8,
"temperature": 0.7,
"maxTokens": 200,
}

response = requests.post(CLOVA_API_URL, headers=headers, json=payload)
response.raise_for_status()
return response.json()
7 changes: 7 additions & 0 deletions roles/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django import forms
from .models import Role

class RoleForm(forms.ModelForm):
class Meta:
model = Role
fields = ['name']
35 changes: 35 additions & 0 deletions roles/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 5.2.4 on 2025-08-07 05:05

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
('teams', '0002_alter_team_invite_code'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='Role',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='roles', to='teams.team')),
],
),
migrations.CreateModel(
name='MemberRoleAssignment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('assigned_by_ai', models.BooleanField(default=False)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='roles.role')),
],
),
]
Empty file added roles/migrations/__init__.py
Empty file.
15 changes: 15 additions & 0 deletions roles/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.db import models
from django.conf import settings
from teams.models import Team # 이미 만든 Team 모델 연결

class Role(models.Model):
name = models.CharField(max_length=50)
team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name='roles')

def __str__(self):
return f"[{self.team.name}] {self.name}"

class MemberRoleAssignment(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
role = models.ForeignKey(Role, on_delete=models.CASCADE)
assigned_by_ai = models.BooleanField(default=False) # AI가 할당했는지 여부
3 changes: 3 additions & 0 deletions roles/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.
13 changes: 13 additions & 0 deletions roles/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.urls import path
from . import views

urlpatterns = [
# 역할 페이지
path('api/dashboard/<int:team_id>/roles/', views.roles_page, name='roles-page'),

# 역할 등록
path('api/dashboard/<int:team_id>/roles/register/', views.register_roles, name='register-roles'),

# AI 역할 추천 API (팀 ID 없어도 됨)
path('roles/ai-recommend-role/', views.recommend_role_api, name='ai-recommend-role'),
]
45 changes: 45 additions & 0 deletions roles/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from django.shortcuts import render, get_object_or_404
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
import json
from .models import Role, MemberRoleAssignment
from teams.models import Team
# from users.models import CustomUser # 유저 모델
from .clova_ai import call_clova_recommendation, make_prompt
from django.shortcuts import render


# 역할 등록
def register_roles(request, team_id):
team = get_object_or_404(Team, id=team_id)
if request.method == 'POST':
roles = request.POST.getlist('roles[]') # ["기획자", "디자이너"]
for r in roles:
Role.objects.create(name=r, team=team)
return JsonResponse({"status": "ok"})

# AI 역할 추천 API
@csrf_exempt
def recommend_role_api(request):
if request.method == "POST":
data = json.loads(request.body)
major = data.get("major")
traits = data.get("traits", [])
preferences = data.get("preferences", [])

prompt = make_prompt(major, traits, preferences)
clova_response = call_clova_recommendation(prompt)

try:
content = clova_response['result']['message']['content']
except:
return JsonResponse({"error": "AI 응답 파싱 실패"}, status=500)

return JsonResponse({"recommended_role": content})

def roles_page(request, team_id):
team = get_object_or_404(Team, id=team_id)
return render(request, 'main/roles.html', {
'team': team,
'current_team_id': team.id # 사이드바에서 쓰기 위해 전달
})
7 changes: 7 additions & 0 deletions teamflow/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
'files',
'schedule',
'team_log',
'roles',
]

# ========================================
Expand Down Expand Up @@ -285,3 +286,9 @@

MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

env = environ.Env()
environ.Env.read_env()

# CLOVA
CLOVA_API_KEY = env("CLOVA_API_KEY")
3 changes: 3 additions & 0 deletions teamflow/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@

path('team-log/', team_log_views.team_log_page, name='team_log_page'), # HTML 페이지

path('', include('roles.urls')),


]

# 개발 환경 static 파일 서빙
Expand Down
4 changes: 2 additions & 2 deletions templates/includes/sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@
</a>
</li>

<!-- 역할 -->
<!-- roles: sidebar.html -->
<li class="nav-item {% if 'roles' in request.path %}active{% endif %}">
<a href="/roles/" class="nav-link">
<a href="{% if team %}{% url 'roles-page' team.id %}{% else %}#{% endif %}" class="nav-link">
<svg class="nav-icon" fill="currentColor" viewBox="0 0 24 24" width="20" height="20">
<path d="M16 4c0-1.11.89-2 2-2s2 .89 2 2-.89 2-2 2-2-.89-2-2zm4 18v-6h2.5l-2.54-7.63A1.999 1.999 0 0 0 18.06 7c-.8 0-1.54.5-1.85 1.26l-1.45 3.63c-.25.65-.65 1.15-1.18 1.46C14.28 13.78 15 14.8 15 16v6h5z"/>
</svg>
Expand Down
58 changes: 58 additions & 0 deletions templates/main/roles.html
Original file line number Diff line number Diff line change
@@ -1 +1,59 @@
<!-- 역할 관리 (AI 추천, 역할 할당) -->
{% extends "base.html" %}
{% load static %}

{% block content %}
<h2>AI 역할 추천</h2>

<form id="ai-form">
<label>전공:
<input type="text" id="major" value="컴퓨터공학과">
</label><br><br>

<label>성향 (쉼표로 구분):
<input type="text" id="traits" value="분석적, 꼼꼼함">
</label><br><br>

<label>선호 작업 (쉼표로 구분):
<input type="text" id="preferences" value="기획, 개발">
</label><br><br>

<button type="submit">AI 추천</button>
</form>

<h3>추천 결과:</h3>
<div id="result-box">-</div>

<script>
document.getElementById("ai-form").addEventListener("submit", async function (e) {
e.preventDefault();

const major = document.getElementById("major").value;
const traits = document.getElementById("traits").value.split(",").map(s => s.trim());
const preferences = document.getElementById("preferences").value.split(",").map(s => s.trim());

const payload = {
major: major,
traits: traits,
preferences: preferences,
};

const response = await fetch("{% url 'ai-recommend-role' %}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": getCSRFToken(),
},
body: JSON.stringify(payload),
});

const data = await response.json();
document.getElementById("result-box").innerText = data.recommended_role || "추천 실패";
});

function getCSRFToken() {
const cookieValue = document.cookie.split('; ').find(row => row.startsWith('csrftoken='));
return cookieValue ? cookieValue.split('=')[1] : '';
}
</script>
{% endblock %}