Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions backend/django_peak/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@
path("tasks/", include("tasks.urls")),
path("today/", include("today.urls")),
path("social/", include("social.urls")),
path("search/", include("search.urls")),
]
19 changes: 18 additions & 1 deletion backend/projects/serializers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from rest_framework import serializers

from .models import Project
from drawers.models import Drawer
from tasks.models import Task
from users.serializers import UserSerializer


class ProjectSerializer(serializers.ModelSerializer):
user = UserSerializer(
default=serializers.CurrentUserDefault(),
Expand Down Expand Up @@ -49,3 +50,19 @@
class Meta: # pyright: ignore [reportIncompatibleVariableOverride] -- ModelSerializer.Meta
model = Project
exclude = ()

class ProjectSearchSerializer(serializers.ModelSerializer):
class Meta:

Check failure on line 55 in backend/projects/serializers.py

View workflow job for this annotation

GitHub Actions / type

"Meta" overrides symbol of same name in class "ModelSerializer"   "backend.projects.serializers.ProjectSearchSerializer.Meta" is not assignable to "rest_framework.serializers.ModelSerializer.Meta"   Type "type[backend.projects.serializers.ProjectSearchSerializer.Meta]" is not assignable to type "type[rest_framework.serializers.ModelSerializer.Meta]" (reportIncompatibleVariableOverride)
model = Project
exclude = ()

class DrawerSearchSerializer(serializers.ModelSerializer):
class Meta:

Check failure on line 60 in backend/projects/serializers.py

View workflow job for this annotation

GitHub Actions / type

"Meta" overrides symbol of same name in class "ModelSerializer"   "backend.projects.serializers.DrawerSearchSerializer.Meta" is not assignable to "rest_framework.serializers.ModelSerializer.Meta"   Type "type[backend.projects.serializers.DrawerSearchSerializer.Meta]" is not assignable to type "type[rest_framework.serializers.ModelSerializer.Meta]" (reportIncompatibleVariableOverride)
model = Drawer
exclude = ()

class TaskSearchSerializer(serializers.ModelSerializer):
class Meta:

Check failure on line 65 in backend/projects/serializers.py

View workflow job for this annotation

GitHub Actions / type

"Meta" overrides symbol of same name in class "ModelSerializer"   "backend.projects.serializers.TaskSearchSerializer.Meta" is not assignable to "rest_framework.serializers.ModelSerializer.Meta"   Type "type[backend.projects.serializers.TaskSearchSerializer.Meta]" is not assignable to type "type[rest_framework.serializers.ModelSerializer.Meta]" (reportIncompatibleVariableOverride)
model = Task
exclude = ()
Comment on lines +54 to +67

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

검색 직렬화기(Serializer) 중복: backend/search/serializers.py에 이미 존재합니다.

ProjectSearchSerializer, DrawerSearchSerializer, TaskSearchSerializer가 이 파일과 backend/search/serializers.py에 모두 정의되어 있습니다. backend/search/serializers.py의 버전은 DrawerSearchSerializerproject.color, TaskSearchSerializerdrawer.project.color를 포함하는 등 프론트엔드 요구사항에 맞는 구현이 되어 있습니다. 이 파일의 중복 정의는 제거하고, Line 4-5의 Drawer, Task import도 함께 제거해야 합니다.

수정 제안
 from .models import Project
-from drawers.models import Drawer
-from tasks.models import Task
 from users.serializers import UserSerializer

그리고 Line 54-67의 세 직렬화기 클래스를 삭제합니다.

🧰 Tools
🪛 GitHub Actions: Backend

[error] 55-55: "Meta" overrides symbol of same name in class "ModelSerializer". "backend.projects.serializers.ProjectSearchSerializer.Meta" is not assignable to "rest_framework.serializers.ModelSerializer.Meta". Type "type[backend.projects.serializers.ProjectSearchSerializer.Meta]" is not assignable to type "type[rest_framework.serializers.ModelSerializer.Meta]" (reportIncompatibleVariableOverride)

🪛 GitHub Check: type

[failure] 65-65:
"Meta" overrides symbol of same name in class "ModelSerializer"
  "backend.projects.serializers.TaskSearchSerializer.Meta" is not assignable to "rest_framework.serializers.ModelSerializer.Meta"
  Type "type[backend.projects.serializers.TaskSearchSerializer.Meta]" is not assignable to type "type[rest_framework.serializers.ModelSerializer.Meta]" (reportIncompatibleVariableOverride)


[failure] 60-60:
"Meta" overrides symbol of same name in class "ModelSerializer"
  "backend.projects.serializers.DrawerSearchSerializer.Meta" is not assignable to "rest_framework.serializers.ModelSerializer.Meta"
  Type "type[backend.projects.serializers.DrawerSearchSerializer.Meta]" is not assignable to type "type[rest_framework.serializers.ModelSerializer.Meta]" (reportIncompatibleVariableOverride)


[failure] 55-55:
"Meta" overrides symbol of same name in class "ModelSerializer"
  "backend.projects.serializers.ProjectSearchSerializer.Meta" is not assignable to "rest_framework.serializers.ModelSerializer.Meta"
  Type "type[backend.projects.serializers.ProjectSearchSerializer.Meta]" is not assignable to type "type[rest_framework.serializers.ModelSerializer.Meta]" (reportIncompatibleVariableOverride)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/projects/serializers.py` around lines 54 - 67, Remove the duplicate
serializers by deleting the ProjectSearchSerializer, DrawerSearchSerializer, and
TaskSearchSerializer class definitions from this module and also remove the
now-unused Drawer and Task imports at the top (the duplicate definitions
conflict with the canonical serializers already defined elsewhere); ensure any
code that referenced these local serializers instead imports the canonical
versions (use the existing serializers that include project.color and
drawer.project.color).


2 changes: 1 addition & 1 deletion backend/projects/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
path("reorder/", views.ProjectReorderView.as_view()),
path("", views.ProjectList.as_view()),
path("inbox/", views.InboxProjectDetail.as_view()),
path("<str:id>/", views.ProjectDetail.as_view()),
path("<str:id>/", views.ProjectDetail.as_view())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

CI 파이프라인 실패 원인: 마지막 항목의 후행 쉼표(trailing comma) 제거

path("<str:id>/", ...) 뒤의 후행 쉼표를 제거한 것이 Ruff 포매팅 검사 실패의 직접적인 원인입니다. Ruff는 멀티라인 컬렉션에서 magic trailing comma를 강제하며, 이를 제거하면 ruff format이 파일을 재포매팅 대상으로 간주합니다. 후행 쉼표를 복원하여 CI를 통과시켜야 합니다.

Proposed fix
-    path("<str:id>/", views.ProjectDetail.as_view())
+    path("<str:id>/", views.ProjectDetail.as_view()),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/projects/urls.py` at line 11, Restore the magic trailing comma after
the path entry so Ruff's multiline-collection rule is satisfied: add a trailing
comma after the call to path("<str:id>/", views.ProjectDetail.as_view()) in the
urls list (the path(...) call referencing views.ProjectDetail.as_view()) so the
file remains formatted and the CI ruff format check passes.

]

urlpatterns = format_suffix_patterns(urlpatterns)
7 changes: 6 additions & 1 deletion backend/projects/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
from rest_framework.response import Response
from rest_framework.exceptions import ValidationError
from rest_framework.pagination import PageNumberPagination
from rest_framework.views import APIView

Check failure on line 5 in backend/projects/views.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F401)

backend/projects/views.py:5:34: F401 `rest_framework.views.APIView` imported but unused

from django.db.models import Q, F, Value

Check failure on line 7 in backend/projects/views.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F401)

backend/projects/views.py:7:36: F401 `django.db.models.Value` imported but unused

Check failure on line 7 in backend/projects/views.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F401)

backend/projects/views.py:7:33: F401 `django.db.models.F` imported but unused

Check failure on line 7 in backend/projects/views.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F401)

backend/projects/views.py:7:30: F401 `django.db.models.Q` imported but unused

from .models import Project
from .serializers import ProjectSerializer, ProjectSerializerForUserProjectList
from drawers.models import Drawer

Check failure on line 10 in backend/projects/views.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F401)

backend/projects/views.py:10:28: F401 `drawers.models.Drawer` imported but unused
from tasks.models import Task

Check failure on line 11 in backend/projects/views.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F401)

backend/projects/views.py:11:26: F401 `tasks.models.Task` imported but unused
from .serializers import ProjectSerializer, ProjectSerializerForUserProjectList, ProjectSearchSerializer, DrawerSearchSerializer, TaskSearchSerializer

Check failure on line 12 in backend/projects/views.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F401)

backend/projects/views.py:12:131: F401 `.serializers.TaskSearchSerializer` imported but unused

Check failure on line 12 in backend/projects/views.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F401)

backend/projects/views.py:12:107: F401 `.serializers.DrawerSearchSerializer` imported but unused

Check failure on line 12 in backend/projects/views.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F401)

backend/projects/views.py:12:82: F401 `.serializers.ProjectSearchSerializer` imported but unused
from .exceptions import ProjectNameDuplicate

from api.permissions import IsUserOwner
Expand Down
Empty file added backend/search/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions backend/search/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

Check failure on line 1 in backend/search/admin.py

View workflow job for this annotation

GitHub Actions / lint

ruff (F401)

backend/search/admin.py:1:28: F401 `django.contrib.admin` imported but unused

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


class SearchConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'search'
Empty file.
3 changes: 3 additions & 0 deletions backend/search/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.db import models

# Create your models here.
25 changes: 25 additions & 0 deletions backend/search/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from rest_framework import serializers

from projects.models import Project
from drawers.models import Drawer
from tasks.models import Task

class ProjectSearchSerializer(serializers.ModelSerializer):
class Meta:

Check failure on line 8 in backend/search/serializers.py

View workflow job for this annotation

GitHub Actions / type

"Meta" overrides symbol of same name in class "ModelSerializer"   "backend.search.serializers.ProjectSearchSerializer.Meta" is not assignable to "rest_framework.serializers.ModelSerializer.Meta"   Type "type[backend.search.serializers.ProjectSearchSerializer.Meta]" is not assignable to type "type[rest_framework.serializers.ModelSerializer.Meta]" (reportIncompatibleVariableOverride)
model = Project
exclude = ()

class DrawerSearchSerializer(serializers.ModelSerializer):
color = serializers.CharField(source="project.color", read_only=True)

class Meta:

Check failure on line 15 in backend/search/serializers.py

View workflow job for this annotation

GitHub Actions / type

"Meta" overrides symbol of same name in class "ModelSerializer"   "backend.search.serializers.DrawerSearchSerializer.Meta" is not assignable to "rest_framework.serializers.ModelSerializer.Meta"   Type "type[backend.search.serializers.DrawerSearchSerializer.Meta]" is not assignable to type "type[rest_framework.serializers.ModelSerializer.Meta]" (reportIncompatibleVariableOverride)
model = Drawer
exclude = ()

class TaskSearchSerializer(serializers.ModelSerializer):
color = serializers.CharField(source="drawer.project.color", read_only=True)

class Meta:

Check failure on line 22 in backend/search/serializers.py

View workflow job for this annotation

GitHub Actions / type

"Meta" overrides symbol of same name in class "ModelSerializer"   "backend.search.serializers.TaskSearchSerializer.Meta" is not assignable to "rest_framework.serializers.ModelSerializer.Meta"   Type "type[backend.search.serializers.TaskSearchSerializer.Meta]" is not assignable to type "type[rest_framework.serializers.ModelSerializer.Meta]" (reportIncompatibleVariableOverride)
model = Task
exclude = ()
Comment on lines +7 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

exclude = ()는 모든 모델 필드를 노출합니다 -- 명시적 fields 사용을 권장합니다.

세 직렬화기 모두 exclude = ()를 사용하여 모델의 모든 필드를 직렬화합니다. 이는 user FK, deleted_at, 내부 관리용 필드 등 프론트엔드에서 불필요하거나 민감한 데이터가 검색 API 응답에 포함될 수 있음을 의미합니다.

최소 권한 원칙에 따라 exclude = () 대신 프론트엔드에서 실제로 사용하는 필드만 명시적으로 fields 리스트로 지정하는 것을 권장합니다. frontend/src/api/search.api.ts의 타입 정의를 참고하여 필요한 필드를 결정할 수 있습니다.

또한, 파이프라인의 pyright 오류를 해결하려면 기존 코드베이스 패턴(backend/projects/serializers.py Line 31 참고)에 맞춰 Meta 클래스에 # pyright: ignore [reportIncompatibleVariableOverride] 주석을 추가하세요.

ProjectSearchSerializer 수정 예시
 class ProjectSearchSerializer(serializers.ModelSerializer):
-    class Meta:
+    class Meta:  # pyright: ignore [reportIncompatibleVariableOverride]
         model = Project
-        exclude = ()
+        fields = ["id", "name", "color", "type", "privacy"]
🧰 Tools
🪛 GitHub Check: type

[failure] 22-22:
"Meta" overrides symbol of same name in class "ModelSerializer"
  "backend.search.serializers.TaskSearchSerializer.Meta" is not assignable to "rest_framework.serializers.ModelSerializer.Meta"
  Type "type[backend.search.serializers.TaskSearchSerializer.Meta]" is not assignable to type "type[rest_framework.serializers.ModelSerializer.Meta]" (reportIncompatibleVariableOverride)


[failure] 15-15:
"Meta" overrides symbol of same name in class "ModelSerializer"
  "backend.search.serializers.DrawerSearchSerializer.Meta" is not assignable to "rest_framework.serializers.ModelSerializer.Meta"
  Type "type[backend.search.serializers.DrawerSearchSerializer.Meta]" is not assignable to type "type[rest_framework.serializers.ModelSerializer.Meta]" (reportIncompatibleVariableOverride)


[failure] 8-8:
"Meta" overrides symbol of same name in class "ModelSerializer"
  "backend.search.serializers.ProjectSearchSerializer.Meta" is not assignable to "rest_framework.serializers.ModelSerializer.Meta"
  Type "type[backend.search.serializers.ProjectSearchSerializer.Meta]" is not assignable to type "type[rest_framework.serializers.ModelSerializer.Meta]" (reportIncompatibleVariableOverride)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/search/serializers.py` around lines 7 - 24, Replace the unsafe
"exclude = ()" in ProjectSearchSerializer, DrawerSearchSerializer, and
TaskSearchSerializer by explicitly listing only the fields the frontend needs
(use the types in frontend/src/api/search.api.ts to determine the exact fields)
in each serializer's Meta.fields; for DrawerSearchSerializer and
TaskSearchSerializer keep the computed "color =
serializers.CharField(source='project.color', read_only=True)" field and include
its name in their Meta.fields as well; finally, add the same "# pyright: ignore
[reportIncompatibleVariableOverride]" comment inside each Meta class (following
the pattern used in backend/projects/serializers.py) to silence the pyright
override warning.


38 changes: 38 additions & 0 deletions backend/search/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from django.db.models import Q, F, Value

from projects.models import Project
from drawers.models import Drawer
from tasks.models import Task
from .serializers import ProjectSearchSerializer, DrawerSearchSerializer, TaskSearchSerializer

SCOPE_BITMASK = {"task": 1, "drawer": 2, "project": 4}
SEARCH_PREVIEW_LIMIT = 4

def global_search(query, scope):
results = dict()

# TODO: global한 결과를 내놓기에 각 결과가 FK object 전체를 들고 오는 것보다 필요한 color만 쓰는 것이 낫다 판단함.
# 검색 옵션에 따라 project, drawer object를 같이 반환하도록 하는 것을 고려
# drawer serializer 참고
targets = {
"task": {"objects": Task.objects.select_related("drawer__project"), "serializer": TaskSearchSerializer},
"drawer": {"objects": Drawer.objects.select_related("project"), "serializer": DrawerSearchSerializer},
"project": {"objects": Project.objects, "serializer": ProjectSearchSerializer},
}

for key, value in targets.items():
if scope & SCOPE_BITMASK[key]:
query_set = value["objects"].filter(
Q(name__icontains=query)
)
count = query_set.count()

if count > SEARCH_PREVIEW_LIMIT:
query_set = query_set[:SEARCH_PREVIEW_LIMIT]

data = value["serializer"](query_set, many=True).data
results[key] = {"data": data, "count": count}
else:
results[key] = {"data": [], "count": 0}
Comment on lines +11 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

사용자 격리 없음 — 전체 사용자 데이터 노출 (보안 취약점)

global_search()user 매개변수를 전혀 받지 않으며, Task.objects, Drawer.objects, Project.objects 각각에 .filter(user=user) 조건이 없습니다. 결과적으로 인증된 모든 사용자가 다른 사용자의 작업, 서랍, 프로젝트를 검색해서 볼 수 있습니다.

함수 시그니처에 user 매개변수를 추가하고 각 쿼리셋에 사용자 필터를 적용해야 합니다.

Proposed fix
-def global_search(query, scope):
+def global_search(query, scope, user):
     results = dict()

     targets = {
-        "task": {"objects": Task.objects.select_related("drawer__project"), "serializer": TaskSearchSerializer},
-        "drawer": {"objects": Drawer.objects.select_related("project"), "serializer": DrawerSearchSerializer},
-        "project": {"objects": Project.objects, "serializer": ProjectSearchSerializer},
+        "task": {"objects": Task.objects.filter(user=user).select_related("drawer__project"), "serializer": TaskSearchSerializer},
+        "drawer": {"objects": Drawer.objects.filter(user=user).select_related("project"), "serializer": DrawerSearchSerializer},
+        "project": {"objects": Project.objects.filter(user=user), "serializer": ProjectSearchSerializer},
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def global_search(query, scope):
results = dict()
# TODO: global한 결과를 내놓기에 각 결과가 FK object 전체를 들고 오는 것보다 필요한 color만 쓰는 것이 낫다 판단함.
# 검색 옵션에 따라 project, drawer object를 같이 반환하도록 하는 것을 고려
# drawer serializer 참고
targets = {
"task": {"objects": Task.objects.select_related("drawer__project"), "serializer": TaskSearchSerializer},
"drawer": {"objects": Drawer.objects.select_related("project"), "serializer": DrawerSearchSerializer},
"project": {"objects": Project.objects, "serializer": ProjectSearchSerializer},
}
for key, value in targets.items():
if scope & SCOPE_BITMASK[key]:
query_set = value["objects"].filter(
Q(name__icontains=query)
)
count = query_set.count()
if count > SEARCH_PREVIEW_LIMIT:
query_set = query_set[:SEARCH_PREVIEW_LIMIT]
data = value["serializer"](query_set, many=True).data
results[key] = {"data": data, "count": count}
else:
results[key] = {"data": [], "count": 0}
def global_search(query, scope, user):
results = dict()
# TODO: global한 결과를 내놓기에 각 결과가 FK object 전체를 들고 오는 것보다 필요한 color만 쓰는 것이 낫다 판단함.
# 검색 옵션에 따라 project, drawer object를 같이 반환하도록 하는 것을 고려
# drawer serializer 참고
targets = {
"task": {"objects": Task.objects.filter(user=user).select_related("drawer__project"), "serializer": TaskSearchSerializer},
"drawer": {"objects": Drawer.objects.filter(user=user).select_related("project"), "serializer": DrawerSearchSerializer},
"project": {"objects": Project.objects.filter(user=user), "serializer": ProjectSearchSerializer},
}
for key, value in targets.items():
if scope & SCOPE_BITMASK[key]:
query_set = value["objects"].filter(
Q(name__icontains=query)
)
count = query_set.count()
if count > SEARCH_PREVIEW_LIMIT:
query_set = query_set[:SEARCH_PREVIEW_LIMIT]
data = value["serializer"](query_set, many=True).data
results[key] = {"data": data, "count": count}
else:
results[key] = {"data": [], "count": 0}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/search/service.py` around lines 11 - 36, global_search currently
lacks user isolation; change the function signature of global_search to accept a
user parameter and apply user-scoped filtering to each target queryset before
applying the name Q filter and pagination. Concretely, update the
construction/usage of targets/values (Task.objects.select_related(...),
Drawer.objects.select_related(...), Project.objects) so you derive a base_qs =
value["objects"].filter(user=user) (or the correct owner field if the model uses
a different name) and then do base_qs.filter(Q(name__icontains=query)); keep the
existing SCOPE_BITMASK check, SEARCH_PREVIEW_LIMIT slice logic, and serializers
(TaskSearchSerializer, DrawerSearchSerializer, ProjectSearchSerializer)
unchanged. Ensure results still return {"data": ..., "count": ...} but only from
the user-scoped querysets.


return results
3 changes: 3 additions & 0 deletions backend/search/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.
11 changes: 11 additions & 0 deletions backend/search/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.urls import path

from rest_framework.urlpatterns import format_suffix_patterns

from . import views

urlpatterns = [
path("", views.GlobalSearchView.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)
88 changes: 88 additions & 0 deletions backend/search/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from rest_framework import mixins, generics, status
from rest_framework.response import Response
from rest_framework.exceptions import ValidationError
from rest_framework.pagination import PageNumberPagination
from rest_framework.views import APIView

from django.db.models import Q, F, Value

from projects.models import Project
from drawers.models import Drawer
from tasks.models import Task
from .serializers import ProjectSearchSerializer, DrawerSearchSerializer, TaskSearchSerializer
from .service import global_search
from projects.exceptions import ProjectNameDuplicate

from api.permissions import IsUserOwner
from api.exceptions import UnknownError
from api.serializers import ReorderSerializer

class SearchPagination(PageNumberPagination):
page_size = 20

# page size 조절 가능하게
page_size_query_param = "page_size"
max_page_size = 100

# 하나의 view에서 여러 serializer를 부를 거라 복잡해도 그냥 APIView가 적합할 거 같음
# 파라미터 종류가 많아지면 django-filter를 적극적으로 고려해 보자...
class ProjectSearchView(APIView):
def get(self, request, *args, **kwargs):
q = request.query_params.get("query", "").strip()

# query가 비어있으면 빈 결과 반환
# TODO: 나중에 프로젝트 페이지와 합치게 되면 전부 보이는 걸로 바뀌어야 할 지도?
if not q:
return Response({
"projects": [],
"drawers": [],
"tasks": [],
})

project_qs = Project.objects.filter(
Q(name__icontains=q)
)

drawer_qs = Drawer.objects.select_related("project").filter(
Q(name__icontains=q)
)

task_qs = Task.objects.select_related("drawer__project").filter(
Q(name__icontains=q)
)

project_data = ProjectSearchSerializer(project_qs, many=True).data
drawer_data = DrawerSearchSerializer(drawer_qs, many=True).data
task_data = TaskSearchSerializer(task_qs, many=True).data

return Response({
"projects": project_data,
"drawers": drawer_data,
"tasks": task_data,
})
Comment on lines +29 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

ProjectSearchViewurls.py에 등록되지 않음 — 사용자 필터도 없음

backend/search/urls.py에는 GlobalSearchView만 등록되어 있고 ProjectSearchView는 어디서도 라우팅되지 않습니다. 또한 project_qs, drawer_qs, task_qs 모두 사용자 필터 없이 전체 테이블을 조회합니다.

이 뷰가 현재 PR에서 의도된 기능이라면 URL 등록 및 사용자 필터를 추가하고, 그렇지 않다면 제거하는 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/search/views.py` around lines 29 - 62, ProjectSearchView is not
routed and its queries lack per-user filtering: register ProjectSearchView in
your URL config (the same module where GlobalSearchView is registered) so the
endpoint is reachable, and restrict project_qs, drawer_qs, and task_qs to the
current user by filtering on ownership/relations (e.g.,
Project.objects.filter(owner=request.user, ...) or using related lookups like
drawer__project__owner=request.user) before serializing; also ensure the view
enforces authentication/permissions (e.g., IsAuthenticated) so request.user is
available.


# GlobalSearchView는 pagination X
class GlobalSearchView(APIView):
def get(self, request, *args, **kwargs):
query = request.query_params.get("keyword", "").strip()
# bitmask: project / drawer / task
scope = request.query_params.get("scope", "7").strip()

try:
scope = int(scope)
except ValueError:
return Response(
{"detail": "search_range must be an integer"},
status=status.HTTP_400_BAD_REQUEST
)

# query가 비어있으면 빈 결과 반환
if not query:
return Response({
"projects": {"data": [], "count": 0},
"drawers": {"data": [], "count": 0},
"tasks": {"data": [], "count": 0},
})
results = global_search(query, scope)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

global_search() 호출 시 user 인자 누락

service.py에서 사용자 필터가 추가되면, 이 호출 지점도 request.user를 전달하도록 수정해야 합니다.

-        results = global_search(query, scope)
+        results = global_search(query, scope, request.user)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
results = global_search(query, scope)
results = global_search(query, scope, request.user)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/search/views.py` at line 86, global_search 호출 시 user 인자가 빠져 있어 사용자
필터가 적용되지 않습니다; views 코드에서 global_search(query, scope) 대신 request.user를 전달하도록
호출부를 수정하여 global_search(query, scope, request.user) 형태로 넘기고, 필요 시 호출자(예: 함수 or
view handler)에서 request 변수가 유효한지 확인한 뒤 전달하세요; 관련 식별자는 global_search와
request.user입니다.


return Response(results)
Comment on lines +79 to +88

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

빈 쿼리 응답의 키 이름이 global_search() 반환값과 불일치

빈 쿼리일 때 반환하는 딕셔너리는 복수형 키("projects", "drawers", "tasks")를 사용하지만, global_search()는 단수형 키("project", "drawer", "task")를 반환합니다(service.pytargets 딕셔너리 키 참조). 쿼리 유무에 따라 프론트엔드가 서로 다른 키를 받게 되므로 파싱 오류가 발생합니다.

Proposed fix
         if not query:
             return Response({
-                "projects": {"data": [], "count": 0},
-                "drawers": {"data": [], "count": 0},
-                "tasks": {"data": [], "count": 0},
+                "project": {"data": [], "count": 0},
+                "drawer": {"data": [], "count": 0},
+                "task": {"data": [], "count": 0},
             })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# query가 비어있으면 빈 결과 반환
if not query:
return Response({
"projects": {"data": [], "count": 0},
"drawers": {"data": [], "count": 0},
"tasks": {"data": [], "count": 0},
})
results = global_search(query, scope)
return Response(results)
# query가 비어있으면 빈 결과 반환
if not query:
return Response({
"project": {"data": [], "count": 0},
"drawer": {"data": [], "count": 0},
"task": {"data": [], "count": 0},
})
results = global_search(query, scope)
return Response(results)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/search/views.py` around lines 79 - 88, The empty-query branch in
backend/search/views.py returns a dict with plural keys
("projects","drawers","tasks") that doesn't match global_search()'s return shape
which uses singular keys ("project","drawer","task" in the targets dict in
service.py); update the early-return payload in the if not query block to use
the same singular keys and value structure (data list and count) as
global_search() so the frontend always receives a consistent response shape from
global_search().

39 changes: 39 additions & 0 deletions frontend/src/api/search.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import client from "@api/client"
import type { Base, PaginationData, Privacy } from "@api/common"

Check failure on line 2 in frontend/src/api/search.api.ts

View workflow job for this annotation

GitHub Actions / lint

'Privacy' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 2 in frontend/src/api/search.api.ts

View workflow job for this annotation

GitHub Actions / lint

'PaginationData' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 2 in frontend/src/api/search.api.ts

View workflow job for this annotation

GitHub Actions / lint

'Base' is defined but never used. Allowed unused vars must match /^_/u
import type { User } from "@api/users.api"

Check failure on line 3 in frontend/src/api/search.api.ts

View workflow job for this annotation

GitHub Actions / lint

'User' is defined but never used. Allowed unused vars must match /^_/u

import type { PaletteColorName } from "@assets/palettes"
import { Project } from "@api/projects.api"
import { Drawer } from "@api/drawers.api"
import { Task } from "@api/tasks.api"

export interface DrawerSearchResult extends Drawer {
color: PaletteColorName
}

export type TaskSearchResult = Task & {
color: PaletteColorName
}

type ResultBlock<T> = {
data: T[]
count: number
}

export interface SearchResponse {
project: ResultBlock<Project>
drawer: ResultBlock<DrawerSearchResult>
task: ResultBlock<TaskSearchResult>
}

export type ProjectType = "inbox" | "regular" | "goal"

//export const getGlobalSearchResults = async (query: string, page: string) => {
export const getGlobalSearchResults = async (query: string) => {
const keyword = query
const res = await client.get<SearchResponse>(`search/`, {
params: { keyword },
})

return res.data
}
8 changes: 8 additions & 0 deletions frontend/src/assets/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,14 @@
"results_title": "Search results for ‘{{query}}’"
}
},
"search": {
"placeholder": "Search your task",
"filter": {
"project": "project",
"drawer": "drawer",
"date": "date"
}
},
"update": {
"message": "New update available.",
"update": "Update",
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/assets/locales/ko/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,14 @@
"results_title": "‘{{query}}’에 대한 검색 결과"
}
},
"search": {
"placeholder": "할 일을 검색해 보세요",
"filter": {
"project": "프로젝트",
"drawer": "서랍",
"date": "날짜"
}
},
"update": {
"message": "새로운 업데이트가 있습니다.",
"update": "업데이트",
Expand Down
Loading
Loading