diff --git a/.github/workflows/generate_dependency_graph.yml b/.github/workflows/generate_dependency_graph.yml
new file mode 100644
index 000000000..1d3390574
--- /dev/null
+++ b/.github/workflows/generate_dependency_graph.yml
@@ -0,0 +1,30 @@
+name: HMH Android Dependency Graph
+
+on:
+ pull_request:
+ branches: [ develop, main ]
+
+jobs:
+ generate-dependency-graph:
+ name: Generate Dependency Graph
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Graphviz
+ uses: ts-graphviz/setup-graphviz@v1
+
+ - name: Generate Dependency Graph
+ run: ./gradlew projectDependencyGraph
+
+ - name: Commmit
+ run: |
+ git config --local user.email 'action@github.com'
+ git config --local user.name 'GitHub Action'
+ git diff --quiet && git diff --staged --quiet || git commit -am 'Update dependency graph'
+
+ - name: Push
+ uses: ad-m/github-push-action@v0.8.0
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/README.md b/README.md
index 5ae414012..01af51853 100644
--- a/README.md
+++ b/README.md
@@ -5,9 +5,14 @@
-# 하면함 Android
-## 스마트폰 중독 탈출, 너도 하면함!
+# 하면함 Android : 스마트폰 중독 탈출, 너도 하면함!
+## HMH Gradle Module Dependency Graph
+
+![](gradle/dependency-graph/project.dot.png)
+
+
+
@@ -17,6 +22,7 @@
+
![Alt](https://repobeats.axiom.co/api/embed/d2c401ae723c367a03ed9fb81ea6e6e7cfbee2ea.svg "Repobeats analytics image")
diff --git a/build.gradle.kts b/build.gradle.kts
index f0cfd694d..0b4eb721a 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -27,6 +27,7 @@ plugins {
alias(libs.plugins.app.distribution) apply false
alias(libs.plugins.crashlytics) apply false
}
+apply(from = file(path = "gradle/projectDependencyGraph.gradle"))
tasks.register("clean", Delete::class) {
delete(rootProject.layout.buildDirectory)
diff --git a/gradle/dependency-graph/project.dot b/gradle/dependency-graph/project.dot
new file mode 100644
index 000000000..ab27db437
--- /dev/null
+++ b/gradle/dependency-graph/project.dot
@@ -0,0 +1,136 @@
+digraph {
+ graph [label="HMH-Android\n ",labelloc=t,fontsize=30,ranksep=1.4];
+ node [style=filled, fillcolor="#bbbbbb"];
+ rankdir=TB;
+
+ # Projects
+
+ ":app" [shape=box, fillcolor="#baffc9"];
+ ":core:common" [fillcolor="#baffc9"];
+ ":core:database" [fillcolor="#baffc9"];
+ ":core:designsystem" [fillcolor="#baffc9"];
+ ":core:domain" [fillcolor="#ffb3ba"];
+ ":core:network" [fillcolor="#baffc9"];
+ ":core:service" [fillcolor="#baffc9"];
+ ":core:viewmodel:main" [fillcolor="#baffc9"];
+ ":data:challenge" [fillcolor="#baffc9"];
+ ":data:device" [fillcolor="#baffc9"];
+ ":data:login" [fillcolor="#baffc9"];
+ ":data:onboarding" [fillcolor="#baffc9"];
+ ":data:usagestats" [fillcolor="#baffc9"];
+ ":data:userinfo" [fillcolor="#baffc9"];
+ ":domain:challenge" [fillcolor="#ffb3ba"];
+ ":domain:login" [fillcolor="#ffb3ba"];
+ ":domain:onboarding" [fillcolor="#ffb3ba"];
+ ":domain:usagestats" [fillcolor="#ffb3ba"];
+ ":domain:userinfo" [fillcolor="#ffb3ba"];
+ ":feature:challenge" [fillcolor="#baffc9"];
+ ":feature:lock" [fillcolor="#baffc9"];
+ ":feature:login" [fillcolor="#baffc9"];
+ ":feature:main" [fillcolor="#baffc9"];
+ ":feature:mypage" [fillcolor="#baffc9"];
+ ":feature:onboarding" [fillcolor="#baffc9"];
+ ":feature:store" [fillcolor="#baffc9"];
+
+ {rank = same; ":app";}
+
+ # Dependencies
+
+ ":app" -> ":feature:login" [style=dotted]
+ ":app" -> ":feature:onboarding" [style=dotted]
+ ":app" -> ":feature:main" [style=dotted]
+ ":app" -> ":feature:mypage" [style=dotted]
+ ":app" -> ":feature:challenge" [style=dotted]
+ ":app" -> ":feature:lock" [style=dotted]
+ ":app" -> ":feature:store" [style=dotted]
+ ":app" -> ":domain:usagestats" [style=dotted]
+ ":app" -> ":domain:userinfo" [style=dotted]
+ ":app" -> ":domain:login" [style=dotted]
+ ":app" -> ":domain:challenge" [style=dotted]
+ ":app" -> ":domain:onboarding" [style=dotted]
+ ":app" -> ":data:usagestats" [style=dotted]
+ ":app" -> ":data:userinfo" [style=dotted]
+ ":app" -> ":data:login" [style=dotted]
+ ":app" -> ":data:challenge" [style=dotted]
+ ":app" -> ":data:device" [style=dotted]
+ ":app" -> ":data:onboarding" [style=dotted]
+ ":app" -> ":core:common" [style=dotted]
+ ":app" -> ":core:designsystem" [style=dotted]
+ ":app" -> ":core:database" [style=dotted]
+ ":app" -> ":core:domain" [style=dotted]
+ ":app" -> ":core:network" [style=dotted]
+ ":app" -> ":core:viewmodel:main" [style=dotted]
+ ":core:common" -> ":core:designsystem" [style=dotted]
+ ":core:service" -> ":core:common" [style=dotted]
+ ":core:service" -> ":core:designsystem" [style=dotted]
+ ":core:service" -> ":core:domain" [style=dotted]
+ ":core:service" -> ":domain:usagestats" [style=dotted]
+ ":core:service" -> ":core:network" [style=dotted]
+ ":core:network" -> ":core:common" [style=dotted]
+ ":core:network" -> ":domain:login" [style=dotted]
+ ":data:usagestats" -> ":domain:usagestats" [style=dotted]
+ ":data:usagestats" -> ":core:database" [style=dotted]
+ ":data:usagestats" -> ":core:network" [style=dotted]
+ ":data:usagestats" -> ":core:domain" [style=dotted]
+ ":data:challenge" -> ":domain:challenge" [style=dotted]
+ ":data:challenge" -> ":core:database" [style=dotted]
+ ":data:challenge" -> ":core:network" [style=dotted]
+ ":data:login" -> ":core:common" [style=dotted]
+ ":data:login" -> ":domain:login" [style=dotted]
+ ":data:login" -> ":core:network" [style=dotted]
+ ":data:device" -> ":domain:challenge" [style=dotted]
+ ":data:device" -> ":core:common" [style=dotted]
+ ":data:device" -> ":core:network" [style=dotted]
+ ":data:userinfo" -> ":domain:userinfo" [style=dotted]
+ ":data:userinfo" -> ":core:network" [style=dotted]
+ ":data:userinfo" -> ":core:common" [style=dotted]
+ ":feature:mypage" -> ":core:common" [style=dotted]
+ ":feature:mypage" -> ":core:designsystem" [style=dotted]
+ ":feature:mypage" -> ":domain:userinfo" [style=dotted]
+ ":feature:mypage" -> ":domain:login" [style=dotted]
+ ":feature:mypage" -> ":core:viewmodel:main" [style=dotted]
+ ":feature:mypage" -> ":core:network" [style=dotted]
+ ":feature:mypage" -> ":feature:store" [style=dotted]
+ ":feature:mypage" -> ":domain:challenge" [style=dotted]
+ ":feature:challenge" -> ":domain:usagestats" [style=dotted]
+ ":feature:challenge" -> ":domain:challenge" [style=dotted]
+ ":feature:challenge" -> ":core:common" [style=dotted]
+ ":feature:challenge" -> ":core:designsystem" [style=dotted]
+ ":feature:challenge" -> ":core:viewmodel:main" [style=dotted]
+ ":feature:challenge" -> ":core:domain" [style=dotted]
+ ":feature:lock" -> ":domain:usagestats" [style=dotted]
+ ":feature:lock" -> ":core:common" [style=dotted]
+ ":feature:lock" -> ":core:designsystem" [style=dotted]
+ ":feature:lock" -> ":core:domain" [style=dotted]
+ ":feature:lock" -> ":core:service" [style=dotted]
+ ":feature:onboarding" -> ":core:common" [style=dotted]
+ ":feature:onboarding" -> ":core:designsystem" [style=dotted]
+ ":feature:onboarding" -> ":core:network" [style=dotted]
+ ":feature:onboarding" -> ":core:service" [style=dotted]
+ ":feature:onboarding" -> ":feature:main" [style=dotted]
+ ":feature:onboarding" -> ":domain:login" [style=dotted]
+ ":feature:onboarding" -> ":domain:challenge" [style=dotted]
+ ":feature:main" -> ":feature:challenge" [style=dotted]
+ ":feature:main" -> ":feature:mypage" [style=dotted]
+ ":feature:main" -> ":core:common" [style=dotted]
+ ":feature:main" -> ":core:designsystem" [style=dotted]
+ ":feature:main" -> ":core:viewmodel:main" [style=dotted]
+ ":feature:main" -> ":core:service" [style=dotted]
+ ":feature:main" -> ":core:network" [style=dotted]
+ ":feature:main" -> ":domain:usagestats" [style=dotted]
+ ":feature:main" -> ":domain:userinfo" [style=dotted]
+ ":feature:store" -> ":core:common" [style=dotted]
+ ":feature:store" -> ":core:designsystem" [style=dotted]
+ ":feature:login" -> ":domain:login" [style=dotted]
+ ":feature:login" -> ":core:common" [style=dotted]
+ ":feature:login" -> ":core:designsystem" [style=dotted]
+ ":feature:login" -> ":core:network" [style=dotted]
+ ":feature:login" -> ":feature:onboarding" [style=dotted]
+ ":domain:usagestats" -> ":core:domain" [style=dotted]
+ ":domain:challenge" -> ":core:domain" [style=dotted]
+ ":core:viewmodel:main" -> ":domain:usagestats" [style=dotted]
+ ":core:viewmodel:main" -> ":domain:challenge" [style=dotted]
+ ":core:viewmodel:main" -> ":domain:userinfo" [style=dotted]
+ ":core:viewmodel:main" -> ":core:common" [style=dotted]
+ ":core:viewmodel:main" -> ":core:domain" [style=dotted]
+}
diff --git a/gradle/dependency-graph/project.dot.png b/gradle/dependency-graph/project.dot.png
new file mode 100644
index 000000000..48be03cc7
Binary files /dev/null and b/gradle/dependency-graph/project.dot.png differ
diff --git a/gradle/projectDependencyGraph.gradle b/gradle/projectDependencyGraph.gradle
new file mode 100644
index 000000000..7eef4172a
--- /dev/null
+++ b/gradle/projectDependencyGraph.gradle
@@ -0,0 +1,125 @@
+// from: https://github.com/JakeWharton/SdkSearch/blob/3351cad9bfacb0a364858e843774147143f58c7a/gradle/projectDependencyGraph.gradle
+task projectDependencyGraph {
+ doLast {
+ def dot = new File(rootProject.rootDir, 'gradle/dependency-graph/project.dot')
+ dot.parentFile.mkdirs()
+ dot.delete()
+
+ dot << 'digraph {\n'
+ dot << " graph [label=\"${rootProject.name}\\n \",labelloc=t,fontsize=30,ranksep=1.4];\n"
+ dot << ' node [style=filled, fillcolor="#bbbbbb"];\n'
+ dot << ' rankdir=TB;\n'
+
+ def rootProjects = []
+ def queue = [rootProject]
+ while (!queue.isEmpty()) {
+ def project = queue.remove(0)
+ rootProjects.add(project)
+ queue.addAll(project.childProjects.values())
+ }
+
+ def projects = new LinkedHashSet()
+ def dependencies = new LinkedHashMap, List>()
+ def multiplatformProjects = []
+ def jsProjects = []
+ def androidProjects = []
+ def androidDynamicFeatureProjects = []
+ def javaProjects = []
+
+ queue = [rootProject]
+ while (!queue.isEmpty()) {
+ def project = queue.remove(0)
+ queue.addAll(project.childProjects.values())
+
+ if (project.plugins.hasPlugin('org.jetbrains.kotlin.multiplatform')) {
+ multiplatformProjects.add(project)
+ }
+ if (project.plugins.hasPlugin('kotlin2js')) {
+ jsProjects.add(project)
+ }
+ if (project.plugins.hasPlugin('com.android.library') ||
+ project.plugins.hasPlugin('com.android.application')) {
+ androidProjects.add(project)
+ }
+ if (project.plugins.hasPlugin('com.android.dynamic-feature')) {
+ androidDynamicFeatureProjects.add(project)
+ }
+ if (project.plugins.hasPlugin('java-library') || project.plugins.hasPlugin('java')) {
+ javaProjects.add(project)
+ }
+
+ project.configurations.all { config ->
+ if (config.name.toLowerCase().contains("test")) return
+ config.dependencies
+ .withType(ProjectDependency)
+ .collect { it.dependencyProject }
+ .each { dependency ->
+ projects.add(project)
+ projects.add(dependency)
+ rootProjects.remove(dependency)
+
+ def graphKey = new Tuple2(project, dependency)
+ def traits = dependencies.computeIfAbsent(graphKey) { new ArrayList() }
+
+ if (config.name.toLowerCase().endsWith('implementation')) {
+ traits.add('style=dotted')
+ }
+ }
+ }
+ }
+
+ projects = projects.sort { it.path }
+
+ dot << '\n # Projects\n\n'
+ for (project in projects) {
+ def traits = []
+
+ if (rootProjects.contains(project)) {
+ traits.add('shape=box')
+ }
+
+ if (multiplatformProjects.contains(project)) {
+ traits.add('fillcolor="#ffd2b3"')
+ } else if (jsProjects.contains(project)) {
+ traits.add('fillcolor="#ffffba"')
+ } else if (androidProjects.contains(project)) {
+ traits.add('fillcolor="#baffc9"')
+ } else if (androidDynamicFeatureProjects.contains(project)) {
+ traits.add('fillcolor="#c9baff"')
+ } else if (javaProjects.contains(project)) {
+ traits.add('fillcolor="#ffb3ba"')
+ } else {
+ traits.add('fillcolor="#eeeeee"')
+ }
+
+ dot << " \"${project.path}\" [${traits.join(", ")}];\n"
+ }
+
+ dot << '\n {rank = same;'
+ for (project in projects) {
+ if (rootProjects.contains(project)) {
+ dot << " \"${project.path}\";"
+ }
+ }
+ dot << '}\n'
+
+ dot << '\n # Dependencies\n\n'
+ dependencies.forEach { key, traits ->
+ dot << " \"${key.first.path}\" -> \"${key.second.path}\""
+ if (!traits.isEmpty()) {
+ dot << " [${traits.join(", ")}]"
+ }
+ dot << '\n'
+ }
+
+ dot << '}\n'
+
+ def p = 'dot -Tpng -O project.dot'.execute([], dot.parentFile)
+ p.waitFor()
+ if (p.exitValue() != 0) {
+ throw new RuntimeException(p.errorStream.text)
+ }
+
+ println("Project module dependency graph created at ${dot.absolutePath}.png")
+ }
+}
\ No newline at end of file